/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

package org.apache.jena.reasoner.rulesys.test;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;

import org.apache.jena.rdf.model.*;
import org.apache.jena.reasoner.InfGraph;
import org.apache.jena.reasoner.Reasoner;
import org.apache.jena.reasoner.ReasonerFactory;
import org.apache.jena.reasoner.rulesys.FBRuleInfGraph;
import org.apache.jena.reasoner.test.WGReasonerTester;
import org.apache.jena.shared.JenaException;
import org.apache.jena.util.FileManager;
import org.apache.jena.vocabulary.RDF;
import org.apache.jena.vocabulary.ReasonerVocabulary;
import org.junit.Assert;
import junit.framework.TestCase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A test harness for running the OWL working group tests. This
 * differs from the RDF one in several ways (separate manifest files,
 * different namespaces, document references lack suffix ...).
 * <p>
 * This version is used for running the core entailment tests as part of unit testing.
 * A separate test harness for use in reporting OWL conformance is being developed and
 * some code rationalization might be once once that stabilizes. </p>
 */
public class OWLWGTester {
    /** The base URI in which the files are purported to reside */
    public static String BASE_URI = "http://www.w3.org/2002/03owlt/";

    /** The base directory in which the test data is actually stored */
    public static String baseDir = "testing/wg/";

    /** The namespace for the test specification schema */
    public static final String NS_OTEST = "http://www.w3.org/2002/03owlt/testOntology#";

    /** The namespace for the test specification schema */
    public static final String NS_RTEST = "http://www.w3.org/2000/10/rdf-tests/rdfcore/testSchema#";

    /** The rdf class for positive tests */
    public static final Resource PositiveEntailmentTest;

    /** The rdf class for positive tests */
    public static final Resource NegativeEntailmentTest;

    /** The predicate defining the description of the test */
    public static final Property descriptionP;

    /** The predicate defining a premise for the test */
    public static final Property premiseDocumentP;

    /** The predicate defining the conclusion from the test */
    public static final Property conclusionDocumentP;

    /** The predicate defining the status of the test */
    public static final Property statusP;

    /** The reasoner factory being tested */
    protected ReasonerFactory reasonerF;

    /** The configuration information for the reasoner */
    protected Resource configuration;

    /** The test case which has invoke this test */
    protected TestCase testcase;

    /** The processing time used since testcase creation */
    protected static long timeCost = 0;

    /** The total number of tests run */
    protected static int numTests = 0;

    protected static Logger logger = LoggerFactory.getLogger(OWLWGTester.class);

    // Static initializer for the predicates
    static {
        PositiveEntailmentTest = ResourceFactory.createProperty(NS_OTEST, "PositiveEntailmentTest");
        NegativeEntailmentTest = ResourceFactory.createProperty(NS_OTEST, "NegativeEntailmentTest");
        descriptionP = ResourceFactory.createProperty(NS_RTEST, "description");
        premiseDocumentP = ResourceFactory.createProperty(NS_RTEST, "premiseDocument");
        conclusionDocumentP = ResourceFactory.createProperty(NS_RTEST, "conclusionDocument");
        statusP = ResourceFactory.createProperty(NS_RTEST, "status");
    }

    /**
     * Constructor
     * @param reasonerF the factory for the reasoner to be tested
     * @param testcase the JUnit test case which is requesting this test
     * @param configuration optional configuration information
     */
    public OWLWGTester(ReasonerFactory reasonerF, TestCase testcase, Resource configuration) {
        this.reasonerF = reasonerF;
        this.testcase = testcase;
        this.configuration = configuration;
    }

    /**
     * Run all the tests in the manifest
     * @param manifestFile the name of the manifest file relative to baseDir
     * @param log set to true to enable derivation logging
     * @param stats set to true to log performance statistics
     * @return true if all the tests pass
     * @throws IOException if one of the test files can't be found
     */
    public boolean runTests(String manifestFile, boolean log, boolean stats) throws IOException {
        // Load up the manifest
        Model manifest = FileManager.getInternal().loadModelInternal(baseDir + manifestFile);
        ResIterator tests = manifest.listResourcesWithProperty(RDF.type, PositiveEntailmentTest);
        while (tests.hasNext()) {
            Resource test = tests.nextResource();
            if (!runTest(test, log, stats)) return false;
        }
        tests = manifest.listResourcesWithProperty(RDF.type, NegativeEntailmentTest);
        while (tests.hasNext()) {
            Resource test = tests.nextResource();
            if (!runTest(test, log, stats)) return false;
        }
        return true;
    }

     /**
     * Run a single designated test.
     * @param test the root node descibing the test
     * @param log set to true to enable derivation logging
     * @param stats set to true to log performance statistics
     * @return true if the test passes
     * @throws IOException if one of the test files can't be found
     */
    public boolean runTest(Resource test, boolean log, boolean stats) throws IOException {
        // Find the specification for the named test
        RDFNode testType = test.getRequiredProperty(RDF.type).getObject();
        if (!(testType.equals(NegativeEntailmentTest) ||
               testType.equals(PositiveEntailmentTest) ) ) {
            throw new JenaException("Can't find test: " + test);
        }

        String description = test.getRequiredProperty(descriptionP).getObject().toString();
        String status = test.getRequiredProperty(statusP).getObject().toString();
        logger.debug("WG test " + test.getURI() + " - " + status);

        // Load up the premise documents
        Model premises = ModelFactory.createDefaultModel();
        for (StmtIterator premisesI = test.listProperties(premiseDocumentP); premisesI.hasNext(); ) {
            premises.add(loadFile(premisesI.nextStatement().getObject().toString() + ".rdf"));
        }

        // Load up the conclusions document
        Resource conclusionsRes = (Resource) test.getRequiredProperty(conclusionDocumentP).getObject();
        Model conclusions = loadFile(conclusionsRes.toString() + ".rdf");

        // Construct the inferred graph
        // Optional logging
        if (log) {
            if (configuration == null) {
                Model m = ModelFactory.createDefaultModel();
                configuration = m.createResource();
            }
            configuration.addProperty(ReasonerVocabulary.PROPtraceOn, "true")
                         .addProperty(ReasonerVocabulary.PROPderivationLogging, "true");
        }
        Reasoner reasoner = reasonerF.create(configuration);
        long t1 = System.currentTimeMillis();
        InfGraph graph = reasoner.bind(premises.getGraph());
        Model result = ModelFactory.createModelForGraph(graph);

        if (stats && graph instanceof FBRuleInfGraph) {
//            ((FBRuleInfGraph)graph).resetLPProfile(true);
        }

        // Check the results against the official conclusions
        boolean correct = true;
        if (testType.equals(PositiveEntailmentTest)) {
            correct = WGReasonerTester.testConclusions(conclusions.getGraph(), result.getGraph());
        } else {
            // A negative entailment check
            correct = !WGReasonerTester.testConclusions(conclusions.getGraph(), result.getGraph());
        }
        long t2 = System.currentTimeMillis();
        timeCost += (t2-t1);
        numTests++;
        if (stats) {
            logger.info("Time=" + (t2-t1) + "ms for " + test.getURI());
            printStats();

            if (graph instanceof FBRuleInfGraph) {
                ((FBRuleInfGraph)graph).printLPProfile();
            }
        }

        if (!correct) {
            // List all the forward deductions for debugging
//            if (graph instanceof FBRuleInfGraph) {
//                System.out.println("Error: deductions graph was ...");
//                FBRuleInfGraph fbGraph = (FBRuleInfGraph)graph;
//                Graph deductions = fbGraph.getDeductionsGraph();
//                org.apache.jena.PrintUtil.printOut(deductions.find(null,null,null));
//            }
        }

        // Signal the results
        if (testcase != null) {
            Assert.assertTrue("Test: " + test + "\n" +  reasonerF.getURI() + "\n" + description, correct);
        }
        return correct;
    }

    /**
     * Utility to load a file as a Model.
     * Files are assumed to be relative to the BASE_URI.
     * @param file the file name, relative to baseDir
     * @return the loaded Model
     */
    public static Model loadFile(String file) throws IOException {
        String langType = "RDF/XML";
        if (file.endsWith(".nt")) {
            langType = "N-TRIPLE";
        } else if (file.endsWith("n3")) {
            langType = "N3";
        }
        Model result = ModelFactory.createDefaultModel();
        String fname = file;
        if (fname.startsWith(BASE_URI)) {
            fname = fname.substring(BASE_URI.length());
        }
        Reader reader = new BufferedReader(new FileReader(baseDir + fname, StandardCharsets.UTF_8));
        result.read(reader, BASE_URI + fname, langType);
        return result;
    }


    /**
     * Log (info level) some summary information on the timecost of the tests.
     */
    public void printStats() {
        logger.info("Ran " + numTests +" in " + timeCost +"ms = " + (timeCost/numTests) + "ms/test");
    }

}
