////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2013 Saxonica Limited.
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

package com.saxonica.testdriver;


//import com.saxonica.testdriver.gui.TestDriverForm;
import net.sf.saxon.Version;
import net.sf.saxon.dom.DOMObjectModel;
import net.sf.saxon.lib.FeatureKeys;
import net.sf.saxon.om.TreeModel;
import net.sf.saxon.option.axiom.AxiomObjectModel;
import net.sf.saxon.option.dom4j.DOM4JObjectModel;
import net.sf.saxon.option.jdom.JDOMObjectModel;
import net.sf.saxon.option.jdom2.JDOM2ObjectModel;
import net.sf.saxon.option.xom.XOMObjectModel;
import net.sf.saxon.s9api.*;

import javax.xml.stream.XMLStreamException;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

public abstract class TestDriver {

    protected String resultsDir = null;
    protected ResultsDocument resultsDoc;
    protected int successes = 0;
    protected int failures = 0;
    protected int notrun = 0;
    protected int wrongErrorResults = 0;
    protected Object guiForm;
    protected boolean unfolded = false;
    protected boolean saveResults = false;
    protected int generateByteCode = 0;
    protected TreeModel treeModel = TreeModel.TINY_TREE;
    protected boolean debug = false;
    protected Pattern testPattern = null;
    protected String requestedTestSet = null;
    protected String testSuiteDir;
    protected Processor driverProc = new Processor(true);
    protected Serializer driverSerializer = driverProc.newSerializer();
    protected HashMap<String, XdmNode> exceptionsMap = new HashMap<String, XdmNode>();
    protected Map<String, Environment> globalEnvironments = new HashMap<String, Environment>();
    protected Map<String, Environment> localEnvironments = new HashMap<String, Environment>();


    public abstract String catalogNamespace();

    public void go(String[] args) throws Exception {

        //AutoActivate.activate(driverProc);
        testSuiteDir = args[0];
        String catalog = args[1];
        String specStr = null;

        for (int i = 2; i < args.length; i++) {
            if (args[i].startsWith("-t:")) {
                testPattern = Pattern.compile(args[i].substring(3));
            }
            if (args[i].startsWith("-s:")) {
                requestedTestSet = args[i].substring(3);
            }
            if (args[i].startsWith("-o")) {
                resultsDir = args[i].substring(3);
            }
            if (args[i].startsWith("-debug")) {
                debug = true;
            }
            if (args[i].equals("-unfolded")) {
                unfolded = true;
            }
            if (args[i].equals("-save")) {
                saveResults = true;
            }
            if (args[i].startsWith("-bytecode")) {
                if (args[i].substring(10).equals("on")) {
                    generateByteCode = 1;
                } else if (args[i].substring(10).equals("debug")) {
                    generateByteCode = 2;
                } else {
                    generateByteCode = 0;
                }
            }
            if (args[i].startsWith("-tree")) {
                if (args[i].substring(6).equalsIgnoreCase("dom")) {
                    treeModel = new DOMObjectModel();
//#if OPT==true
                } else if (args[i].substring(6).equalsIgnoreCase("jdom")) {
                    treeModel = new JDOMObjectModel();
                } else if (args[i].substring(6).equalsIgnoreCase("jdom2")) {
                    treeModel = new JDOM2ObjectModel();
                } else if (args[i].substring(6).equalsIgnoreCase("dom4j")) {
                    treeModel = new DOM4JObjectModel();
                } else if (args[i].substring(6).equalsIgnoreCase("xom")) {
                    treeModel = new XOMObjectModel();
                } else if (args[i].substring(6).equalsIgnoreCase("axiom")) {
                    treeModel = new AxiomObjectModel();
//#endif
                } else if (args[i].substring(6).equalsIgnoreCase("tinytree")) {
                    treeModel = TreeModel.TINY_TREE;
                } else if (args[i].substring(6).equalsIgnoreCase("condensed")) {
                    treeModel = TreeModel.TINY_TREE_CONDENSED;
                } else if (args[i].substring(6).equalsIgnoreCase("linked")) {
                    treeModel = TreeModel.LINKED_TREE;
                } else {
                    throw new Exception("The TreeModel specified does not exist");
                }
            }
            if (args[i].startsWith("-lang")) {
                specStr = args[i].substring(6);
                processSpec(specStr);
            }
        }
        if (resultsDoc == null) {
            printError("No result document: missing -lang option", "");
            if (guiForm == null) {
                System.exit(2);
            }
        }
        if (resultsDir == null) {
            printError("No results directory specified (use -o:dirname)", "");
            if (guiForm == null) {
                System.exit(2);
            }
        }

        driverSerializer.setOutputStream(System.err);
        driverSerializer.setOutputProperty(Serializer.Property.OMIT_XML_DECLARATION, "yes");
        processCatalog(new File(catalog));
        printResults(resultsDir + "/results" + Version.getProductVersion() + ".xml");
    }

    public String getResultsDir() {
        return resultsDir;
    }

    public abstract void processSpec(String specStr);


    private void processCatalog(File catalogFile) throws SaxonApiException {
//#if EE==true
        if (generateByteCode == 1) {
            driverProc.setConfigurationProperty(FeatureKeys.GENERATE_BYTE_CODE, "true");
            driverProc.setConfigurationProperty(FeatureKeys.DEBUG_BYTE_CODE, "false");
        } else if (generateByteCode == 2) {
            driverProc.setConfigurationProperty(FeatureKeys.GENERATE_BYTE_CODE, "true");
            driverProc.setConfigurationProperty(FeatureKeys.DEBUG_BYTE_CODE, "true");
        } else {
            driverProc.setConfigurationProperty(FeatureKeys.GENERATE_BYTE_CODE, "false");
            driverProc.setConfigurationProperty(FeatureKeys.DEBUG_BYTE_CODE, "false");
        }
//#endif
        DocumentBuilder catbuilder = driverProc.newDocumentBuilder();
        catbuilder.setTreeModel(treeModel);
        XdmNode catalog = catbuilder.build(catalogFile);
        XPathCompiler xpc = driverProc.newXPathCompiler();
        xpc.setLanguageVersion("3.0");
        xpc.setCaching(true);
        xpc.declareNamespace("", catalogNamespace());

        createGlobalEnvironments(catalog, xpc);

        try {
            writeResultFilePreamble(driverProc, catalog);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        readExceptionsFile();


        if (requestedTestSet != null) {
            try {
                XdmNode funcSetNode = (XdmNode) xpc.evaluateSingle("//test-set[@name='" + requestedTestSet + "']", catalog);
                if (funcSetNode == null) {
                    throw new Exception("Test-set " + requestedTestSet + " not found!");
                }
                processTestSet(catbuilder, xpc, funcSetNode);
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        } else {
            for (XdmItem testSet : xpc.evaluate("//test-set", catalog)) {
                processTestSet(catbuilder, xpc, ((XdmNode) testSet));
            }
        }
        try {
            writeResultFilePostamble();
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

    /**
     * Look for an exceptions.xml document with the general format:
     * <p/>
     * <exceptions xmlns="...test catalog namespace...">
     * <exception test-set ="testset1" test-case="testcase" run="yes/no/not-unfolded"
     * bug="bug-reference" reason="">
     * <results>
     * ... alternative expected results ...
     * </results>
     * <optimization>
     * ... assertions about the "explain" tree
     * </optimization>
     * </exception>
     * </exceptions>
     */

    protected void readExceptionsFile() {

        XdmNode exceptionsDoc = null;
        DocumentBuilder exceptBuilder = driverProc.newDocumentBuilder();
        QName testCase = new QName("", "test-case");
        try {
            exceptionsDoc = exceptBuilder.build(new File(resultsDir + "/exceptions.xml"));
            XdmSequenceIterator iter = exceptionsDoc.axisIterator(Axis.DESCENDANT, new QName("", "exception"));
            while (iter.hasNext()) {
                XdmNode entry = (XdmNode) iter.next();
                String test = entry.getAttributeValue(testCase);
                if (test != null) {
                    exceptionsMap.put(test, entry);
                }
            }
        } catch (SaxonApiException e) {
            printError("*** Failed to process exceptions file: ", e.getMessage());
        }

    }

    protected abstract void createGlobalEnvironments(
            XdmNode catalog, XPathCompiler xpc)
            throws SaxonApiException;

    protected void createLocalEnvironments(XdmNode testSetDocNode) {
        localEnvironments.clear();
        Environment defaultEnvironment =
                Environment.createLocalEnvironment(testSetDocNode.getBaseURI(), generateByteCode, unfolded);
        localEnvironments.put("default", defaultEnvironment);
    }

    protected Environment getEnvironment(XdmNode testCase, XPathCompiler xpc) throws SaxonApiException {
        String testCaseName = testCase.getAttributeValue(new QName("name"));
        XdmNode environmentNode = (XdmNode) xpc.evaluateSingle("environment", testCase);
        Environment env;
        if (environmentNode == null) {
            env = localEnvironments.get("default");
        } else {
            String envName = environmentNode.getAttributeValue(new QName("ref"));
            if (envName == null || envName.equals("")) {
                boolean baseUriCheck = false;
                env= null;
                try{
                env = Environment.processEnvironment(this, xpc, environmentNode, null, localEnvironments.get("default"));
                baseUriCheck = ((XdmAtomicValue) xpc.evaluateSingle("static-base-uri/@uri='#UNDEFINED'", environmentNode)).getBooleanValue();
                } catch(NullPointerException ex){
                    System.err.println("Failure loading environment");
                }
                if (baseUriCheck) {
                    //writeTestcaseElement(testCaseName, "notRun", "static-base-uri not supported", null);
                    return null;
                }
            } else {
                env = localEnvironments.get(envName);
                if (env == null) {
                    env = globalEnvironments.get(envName);
                }
                if (env == null) {
                    for (XdmItem e : xpc.evaluate("//environment[@name='" + envName + "']", testCase)) {
                        Environment.processEnvironment(this, xpc, e, localEnvironments, localEnvironments.get("default"));
                    }
                    env = localEnvironments.get(envName);
                }
                if (env == null) {
                    println("*** Unknown environment " + envName);
                    //writeTestcaseElement(testCaseName, "fail", "Environment " + envName + " not found", null);
                    failures++;
                    return null;
                }

            }
        }
        return env;
    }

    protected void writeResultFilePreamble(Processor processor, XdmNode catalog)
            throws IOException, SaxonApiException, XMLStreamException, Exception {
        resultsDoc.writeResultFilePreamble(processor, catalog);
    }

    protected void writeResultFilePostamble()
            throws XMLStreamException {
        resultsDoc.writeResultFilePostamble();
    }

    protected void startTestSetElement(XdmNode testSetNode) {
        resultsDoc.startTestSetElement(testSetNode);
    }

    protected void writeTestSetEndElement() {
        resultsDoc.endElement();
    }


    private void processTestSet(DocumentBuilder catbuilder, XPathCompiler xpc, XdmNode testSetNode) throws SaxonApiException {
        String testName;
        String testSet;
        startTestSetElement(testSetNode);
        File testSetFile = new File(testSuiteDir + "/" + testSetNode.getAttributeValue(new QName("file")));
        XdmNode testSetDocNode = catbuilder.build(testSetFile);
        createLocalEnvironments(testSetDocNode);
        boolean run = true;
        // TODO: this won't pick up any test-set level dependencies in the XSLT 3.0 catalog
        if (((XdmAtomicValue) xpc.evaluate("exists(/test-set/dependency)", testSetDocNode).itemAt(0)).getBooleanValue()) {
            for (XdmItem dependency : xpc.evaluate("/test-set/dependency", testSetDocNode)) {
                if (!dependencyIsSatisfied((XdmNode) dependency, localEnvironments.get("default"))) {
                    run = false;
                }
            }
        }
        if (run) {
            if (testPattern == null) {
                for (XdmItem env : xpc.evaluate("//environment[@name]", testSetDocNode)) {
                    try{
                    Environment.processEnvironment(this, xpc, env, localEnvironments, localEnvironments.get("default"));
                    }catch(NullPointerException ex) {
                         System.err.println("Failure loading environment");
                    }
                }
            }
            testSet = xpc.evaluateSingle("/test-set/@name", testSetDocNode).getStringValue();
            for (XdmItem testCase : xpc.evaluate("//test-case", testSetDocNode)) {

                testName = xpc.evaluateSingle("@name", testCase).getStringValue();
                if (testPattern != null && !testPattern.matcher(testName).matches()) {
                    continue;
                }
                println("-s:" + testSet + " -t:" + testName);

                runTestCase((XdmNode) testCase, xpc);
            }
        }
        writeTestSetEndElement();
    }

    protected abstract void runTestCase(XdmNode testCase, XPathCompiler catalogXpc)
            throws SaxonApiException;

//    public void setTestDriverForm(TestDriverForm gui) {
//        guiForm = gui;
//    }

    public void println(String data) {
        if (guiForm != null) {
            //guiForm.setResultTextArea(data);
        } else {
            System.err.println(data);
        }
    }

    public void printResults(String resultsFileStr) {
        if (guiForm != null) {
            //guiForm.printResults("Result: " + successes + " successes, " + failures + " failures, " + wrongErrorResults + " incorrect ErrorCode, " + notrun + " not run", resultsFileStr, resultsDir);
        } else {
            System.err.println(successes + " successes, " + failures + " failures, " + wrongErrorResults + " incorrect ErrorCode, " + notrun + " not run");
        }
    }

    public void printError(String error, String message) {
        if (guiForm != null) {
            //guiForm.errorPopup(error);
            System.err.println(error + message);
        } else {
            System.err.println(error + message);
        }
    }

    public abstract boolean dependencyIsSatisfied(XdmNode dependency, Environment env);

}

