package com.saxonica.testdriver;


import net.sf.saxon.Configuration;
import net.sf.saxon.Version;
import net.sf.saxon.lib.EnvironmentVariableResolver;
import net.sf.saxon.lib.FeatureKeys;
import net.sf.saxon.lib.OutputURIResolver;
import net.sf.saxon.s9api.*;
import net.sf.saxon.trans.XPathException;

import javax.xml.stream.XMLStreamException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * This class runs the W3C XSLT Test Suite, driven from the test catalog.
 */
public class Xslt30TestSuiteDriver extends TestDriver {

    public static void main(String[] args) throws Exception {
        if (args.length == 0 || args[0].equals("-?")) {
            System.err.println("java com.saxonica.testdriver.Xslt30TestSuiteDriver testsuiteDir catalog [-o:resultsdir] [-s:testSetName]" +
                    " [-t:testNamePattern] [-bytecode:on|off|debug] [-tree] [-lang] [-save]");
        }

        System.err.println("Testing Saxon " + Version.getProductVersion());
        new Xslt30TestSuiteDriver().go(args);
    }


    @Override
    public String catalogNamespace() {
        return "http://www.w3.org/2012/10/xslt-test-catalog";
    }

    @Override
    protected void writeResultFilePreamble(Processor processor, XdmNode catalog)
            throws IOException, SaxonApiException, XMLStreamException, Exception {
        //resultsDoc = new Xslt30ResultsDocument(this, Spec.XT30);
        super.writeResultFilePreamble(processor, catalog);
    }

    @Override
    public void processSpec(String specStr) {
        resultsDoc = new Xslt30ResultsDocument(this, Spec.XT30);
        // No action: always use XSLT
    }

    @Override
    protected void createGlobalEnvironments(XdmNode catalog, XPathCompiler xpc) throws SaxonApiException {
        Environment environment = null;
        for (XdmItem env : xpc.evaluate("//environment", catalog)) {
            environment = Environment.processEnvironment(
                    this, xpc, env, globalEnvironments, localEnvironments.get("default"));
        }
        //buildDependencyMap(driverProc, environment);
    }

    private boolean isSlow(String testName) {
        return testName.startsWith("regex-classes") ||
                testName.equals("normalize-unicode-008");
    }


    @Override
    protected void runTestCase(XdmNode testCase, XPathCompiler xpath) throws SaxonApiException {

        final TestOutcome outcome = new TestOutcome(this);
        String testName = testCase.getAttributeValue(new QName("name"));
        String testSetName = testCase.getParent().getAttributeValue(new QName("name"));

        if (exceptionsMap.containsKey(testName)) {
            notrun++;
            resultsDoc.writeTestcaseElement(testName, "notRun", exceptionsMap.get(testName).getAttributeValue(new QName("reason")));
            return;
        }

        if (exceptionsMap.containsKey(testName) || isSlow(testName)) {
            notrun++;
            resultsDoc.writeTestcaseElement(testName, "notRun", "requires excessive resources");
            return;
        }

        XdmValue specAtt = xpath.evaluateSingle("(/test-set/dependencies/spec/@value, ./dependencies/spec/@value)[last()]", testCase);
        String spec = specAtt.toString();

        final Environment env = getEnvironment(testCase, xpath);
        if (env == null) {
            resultsDoc.writeTestcaseElement(testName, "notRun", "test catalog error");
            return;
        }

        if (testName.contains("environment-variable")) {
                        EnvironmentVariableResolver resolver = new EnvironmentVariableResolver() {
                    public Set<String> getAvailableEnvironmentVariables() {
                        Set<String> strings = new HashSet<String>();
                        strings.add("QTTEST");
                        strings.add("QTTEST2");
                        strings.add("QTTESTEMPTY");
                        return strings;
                    }

                    public String getEnvironmentVariable(String name) {
                        if (name.equals("QTTEST")) {
                            return "42";
                        } else if (name.equals("QTTEST2")) {
                            return "other";
                        } else if (name.equals("QTTESTEMPTY")) {
                            return "";
                        } else {
                            return null;
                        }
                    }
                };
            env.processor.setConfigurationProperty(FeatureKeys.ENVIRONMENT_VARIABLE_RESOLVER, resolver);
        }
        XdmNode testInput = (XdmNode) xpath.evaluateSingle("test", testCase);
        XdmNode stylesheet = (XdmNode) xpath.evaluateSingle("stylesheet", testInput);


        for (XdmItem dep : xpath.evaluate("(/test-set/dependencies/*, ./dependencies/*)", testCase)) {
            if (!dependencyIsSatisfied((XdmNode)dep, env)) {
                notrun++;
                resultsDoc.writeTestcaseElement(testName, "notRun", "dependency not satisfied");
                return;
            }
        }

        XsltExecutable sheet = env.xsltExecutable;
        ErrorCollector collector = new ErrorCollector();

        if (stylesheet != null) {
            String fileName = stylesheet.getAttributeValue(new QName("file"));

            Source styleSource = new StreamSource(testCase.getBaseURI().resolve(fileName).toString());

            XsltCompiler compiler = env.xsltCompiler;
            compiler.setXsltLanguageVersion(spec.contains("XSLT30")||spec.contains("XSLT20+") ? "3.0" : "2.0");
            compiler.setErrorListener(collector);
            try {
                sheet = compiler.compile(styleSource);
            } catch (SaxonApiException err) {
                outcome.setException(err);
                outcome.setErrorsReported(collector.getErrorCodes());
            }
        }

        if (sheet != null) {
            XdmItem contextItem = env.contextItem;
            QName initialMode = getQNameAttribute(xpath, testInput, "initial-mode/@name");
            QName initialTemplate = getQNameAttribute(xpath, testInput, "initial-template/@name");

            try {
                XsltTransformer transformer = sheet.load();
                transformer.setURIResolver(env);
                if (env.unparsedTextResolver != null) {
                    transformer.getUnderlyingController().setUnparsedTextURIResolver(env.unparsedTextResolver);
                }
                if (initialTemplate != null) {
                    transformer.setInitialTemplate(initialTemplate);
                }
                if (initialMode != null) {
                    transformer.setInitialMode(initialMode);
                }
                for (XdmItem param : xpath.evaluate("param", testInput)) {
                    String name = ((XdmNode)param).getAttributeValue(new QName("name"));
                    String select = ((XdmNode) param).getAttributeValue(new QName("select"));
                    XdmValue value = null;
                    try {
                        value = xpath.evaluate(select, null);
                    } catch (SaxonApiException e) {
                        System.err.println("*** Error evaluating parameter " + name + ": " + e.getMessage());
                        throw e;
                    }
                    transformer.setParameter(new QName(name), value);
                }
                if (contextItem != null) {
                    transformer.setInitialContextNode((XdmNode)contextItem);
                }
                if (env.streamedSource != null) {
                    transformer.setSource(new StreamSource(env.streamedSource));
                }
                for (QName varName : env.params.keySet()) {
                    transformer.setParameter(varName, env.params.get(varName));
                }
                transformer.setErrorListener(collector);
                transformer.setBaseOutputURI(new File(resultsDir + "/results/output.xml").toURI().toString());
                transformer.setMessageListener(new MessageListener() {
                    public void message(XdmNode content, boolean terminate, SourceLocator locator) {
                        outcome.addXslMessage(content);
                    }
                });


                // Run the transformation twice, once for serialized results, once for a tree.
                // TODO: we could be smarter about this and capture both

                // run with serialization
                StringWriter sw = new StringWriter();
                Serializer serializer = env.processor.newSerializer(sw);
                transformer.setDestination(serializer);
                transformer.getUnderlyingController().setOutputURIResolver(
                    new OutputResolver(env.processor, outcome, true));
                transformer.transform();
                outcome.setPrincipalSerializedResult(sw.toString());
                if (saveResults) {
                    // currently, only save the principal result file
                    saveResultsToFile(sw.toString(),
                            new File(resultsDir + "/results/" + testSetName + "/" + testName + ".out"));
                    Map<URI, TestOutcome.SingleResultDoc> xslResultDocuments = outcome.getSecondaryResultDocuments();
                    for (Map.Entry<URI, TestOutcome.SingleResultDoc> entry : xslResultDocuments.entrySet()) {
                        URI key = entry.getKey();
                        String path = key.getPath();
                        String serialization = outcome.serialize(env.processor, entry.getValue());
                        saveResultsToFile(serialization, new File(path));
                    }
                }

                // run without serialization
                XdmDestination destination = new XdmDestination();
                transformer.setDestination(destination);
                transformer.getUnderlyingController().setOutputURIResolver(
                    new OutputResolver(env.processor, outcome, false));
                transformer.transform();
                outcome.setPrincipalResult(destination.getXdmNode());
                //}
            } catch (SaxonApiException err) {
                outcome.setException(err);
                outcome.setErrorsReported(collector.getErrorCodes());
            } catch (Exception err) {
                err.printStackTrace();
                failures++;
                resultsDoc.writeTestcaseElement(testName, "fail", "*** crashed " + err.getClass() + ": " + err.getMessage());
                return;
            }
        }
        XdmNode assertion = (XdmNode) xpath.evaluateSingle("result/*", testCase);
        if (assertion == null) {
            failures++;
            resultsDoc.writeTestcaseElement(testName, "fail", "No test assertions found");
            return;
        }
        XPathCompiler assertionXPath = env.processor.newXPathCompiler();
        //assertionXPath.setLanguageVersion("3.0");
        boolean success = outcome.testAssertion(assertion, outcome.getPrincipalResultDoc(), assertionXPath, xpath, debug);
        if (success) {
            if (outcome.getWrongErrorMessage() != null) {
                outcome.setComment(outcome.getWrongErrorMessage());
                wrongErrorResults++;
            } else {
                successes++;
            }
            resultsDoc.writeTestcaseElement(testName, "pass", outcome.getComment());
        } else {
            failures++;
            resultsDoc.writeTestcaseElement(testName, "fail", outcome.getComment());
        }
    }

    private boolean mustSerialize(XdmNode testCase, XPathCompiler xpath) throws SaxonApiException{
        return saveResults ||
                ((XdmAtomicValue) xpath.evaluateSingle(
                "exists(./result//(assert-serialization-error|serialization-matches|assert-serialization)[not(parent::*[self::assert-message|self::assert-result-document])])", testCase)).getBooleanValue();
    }

    private void saveResultsToFile(String content, File file) {
        try {
            if (!file.exists()) {
                File directory = file.getParentFile();
                if (directory != null && !directory.exists()) {
                    directory.mkdirs();
                }
                file.createNewFile();
            }
            FileWriter writer = new FileWriter(file);
            writer.append(content);
            writer.close();
        } catch (IOException e) {
            System.err.println("*** Failed to save results to " + file.getAbsolutePath());
            e.printStackTrace();
        }
    }


    @Override
    public boolean dependencyIsSatisfied(XdmNode dependency, Environment env) {
        String type = dependency.getNodeName().getLocalName();
        String value = dependency.getAttributeValue(new QName("value"));
        boolean inverse = "false".equals(dependency.getAttributeValue(new QName("satisfied")));
        if ("spec".equals(type)) {
            return true;
        } else if ("feature".equals(type)) {
//            <xs:enumeration value="backwards_compatibility" />
//            <xs:enumeration value="disabling_output_escaping" />
//            <xs:enumeration value="schema_aware" />
//            <xs:enumeration value="namespace_axis" />
//            <xs:enumeration value="streaming" />
//            <xs:enumeration value="XML_1.1" />

            if ("XML_1.1".equals(value) && !inverse) {
                if (env != null) {
                    env.processor.setXmlVersion("1.1");
                    return true;
                } else {
                    return false;
                }
            } else if ("disabling_output_escaping".equals(value)) {
                return !inverse;
            } else if ("schema_aware".equals(value)) {
                if (!treeModel.isSchemaAware() && !inverse) {
                    return false; // cannot use the selected tree model for schema-aware tests
                }
                if (env != null && env.processor.isSchemaAware() != !inverse) {
                    // Don't attempt to run non-SA tests with an SA processor; the presence of constructs like
                    // import schema will switch on schema-awareness.
                    return false;
                }
                env.xsltCompiler.setSchemaAware(!inverse);
                return true;
            } else if ("namespace_axis".equals(value)) {
                return !inverse;
            } else if ("streaming".equals(value)) {
                return !inverse;
            } else if ("backwards_compatibility".equals(value)) {
                return !inverse;
            }
            return false;
        } else if ("xsd-version".equals(type)) {
            if ("1.1".equals(value)) {
                if (env != null) {
                    env.processor.setConfigurationProperty(FeatureKeys.XSD_VERSION, (inverse ? "1.0" : "1.1"));
                } else {
                    return false;
                }
            } else if ("1.0".equals(value)) {
                if (env != null) {
                    env.processor.setConfigurationProperty(FeatureKeys.XSD_VERSION, (inverse ? "1.1" : "1.0"));
                } else {
                    return false;
                }
            }
            return true;
        } else if ("available_documents".equals(type)) {
            return !inverse;
        } else if ("default_language_for_numbering".equals(type)) {
            return !inverse;
        } else if ("languages_for_numbering".equals(type)) {
            return !inverse;
        } else if ("supported_calendars_in_date_formatting_functions".equals(type)) {
            return !inverse;
        } else if ("default_calendar_in_date_formatting_functions".equals(type)) {
            return !inverse;
        } else if ("maximum_number_of_decimal_digits".equals(type)) {
            return !inverse;
//        } else if ("collation_uri".equals(type)) {
//            return !inverse;
//        } else if ("statically_known_collations".equals(type)) {
//            if (value.equals("http://www.w3.org/xslts/collation/caseblind") && !inverse) {
//                env.processor.getUnderlyingConfiguration().setCollationURIResolver(
//                        new StandardCollationURIResolver() {
//                            public StringCollator resolve(String uri, String base, Configuration config) {
//                                if ("http://www.w3.org/xslts/collation/caseblind".equals(uri)) {
//                                    return super.resolve("http://saxon.sf.net/collation?ignore-case=yes", "", config);
//                                } else {
//                                    return super.resolve(uri, base, config);
//                                }
//                            }
//                        }
//                );
//            }
//            // Alternative case-blind collation URI used in QT3 tests
//            if (value.equals("http://www.w3.org/2010/09/qt-fots-catalog/collation/caseblind") && !inverse) {
//                env.processor.getUnderlyingConfiguration().setCollationURIResolver(
//                        new StandardCollationURIResolver() {
//                            public StringCollator resolve(String uri, String base, Configuration config) {
//                                if ("http://www.w3.org/2010/09/qt-fots-catalog/collation/caseblind".equals(uri)) {
//                                    return super.resolve("http://saxon.sf.net/collation?ignore-case=yes", "", config);
//                                } else {
//                                    return super.resolve(uri, base, config);
//                                }
//                            }
//                        }
//                );
//            }
//            return true;
        } else if ("default_output_encoding".equals(type)) {
            return !inverse;
        } else if ("unparsed_text_encoding".equals(type)) {
            return !inverse;
        } else if ("year_component_values".equals(type)) {
            return !inverse;
        } else if ("additional_normalization_form".equals(type)) {
            return !inverse;
        } else if ("recognize_id_as_uri_fragment".equals(type)) {
            return !inverse;
        } else if ("on-multiple-match".equals(type)) {
            if (value.equals("error")) {
                env.xsltCompiler.getUnderlyingCompilerInfo().setRecoveryPolicy(Configuration.DO_NOT_RECOVER);
            } else {
                env.xsltCompiler.getUnderlyingCompilerInfo().setRecoveryPolicy(Configuration.RECOVER_SILENTLY);
            }
            return true;
        } else if ("ignore-doc-failure".equals(type)) {
            if (value.equals("false")) {
                env.xsltCompiler.getUnderlyingCompilerInfo().setRecoveryPolicy(Configuration.DO_NOT_RECOVER);
            } else {
                env.xsltCompiler.getUnderlyingCompilerInfo().setRecoveryPolicy(Configuration.RECOVER_SILENTLY);
            }
            return true;
        } else if ("combinations_for_numbering".equals(type)) {
            return !inverse;
        } else {
            println("**** dependency not recognized: " + type);
            return false;
        }
    }

    private static String getCanonicalPath(File file) {
        try {
            return file.getCanonicalPath();
        } catch (IOException err) {
            return file.getAbsolutePath();
        }
    }

    private static QName getQNameAttribute(XPathCompiler xpath, XdmItem contextItem, String attributePath) throws SaxonApiException {
        String exp = "for $att in " + attributePath +
                " return if (contains($att, ':')) then resolve-QName($att, $att/..) else QName('', $att)";
        XdmAtomicValue qname = (XdmAtomicValue) xpath.evaluateSingle(exp, contextItem);
        return (qname == null ? null : (QName) qname.getValue());
    }

    private static class OutputResolver implements OutputURIResolver {

        private Processor proc;
        private TestOutcome outcome;
        private Destination destination;
        private StringWriter stringWriter;
        boolean serialized;
        URI uri;

        public OutputResolver(Processor proc, TestOutcome outcome, boolean serialized) {
            this.proc = proc;
            this.outcome = outcome;
            this.serialized = serialized;
        }

        public OutputResolver newInstance() {
            return new OutputResolver(proc, outcome, serialized);
        }

        public Result resolve(String href, String base) throws XPathException {
            try {
                uri = new URI(base).resolve(href);
                if (serialized) {
                    //destination = proc.newSerializer();
                    stringWriter = new StringWriter();
                    StreamResult result =  new StreamResult(stringWriter);
                    result.setSystemId(uri.toString());
                    return result;
//                    ((Serializer)destination).setOutputWriter(stringWriter);
//                    Receiver r = destination.getReceiver(proc.getUnderlyingConfiguration());
//                    r.setSystemId(uri.toString());
//                    return r;
                } else {
                    destination = new XdmDestination();
                    ((XdmDestination)destination).setBaseURI(uri);
                    return destination.getReceiver(proc.getUnderlyingConfiguration());
                }
            } catch (SaxonApiException e) {
                throw new XPathException(e);
            } catch (URISyntaxException e) {
                throw new XPathException(e);
            }
        }

        public void close(Result result) throws XPathException {
            if (serialized) {
                outcome.setSecondaryResult(uri, null, stringWriter.toString());
            } else {
                XdmDestination xdm = (XdmDestination)destination;
                outcome.setSecondaryResult(xdm.getBaseURI(), xdm.getXdmNode(), null);
            }
        }

    }
}
