/*

   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

       http://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.

 */
package org.apache.batik.test.xml;

import java.io.File;
import java.io.StringWriter;
import java.io.PrintWriter;

import java.net.URL;
import java.net.MalformedURLException;

import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;

import org.apache.batik.test.DefaultTestSuite;
import org.apache.batik.test.DefaultTestReport;
import org.apache.batik.test.TestReport;
import org.apache.batik.test.TestSuite;
import org.apache.batik.test.Test;
import org.apache.batik.test.TestFilter;
import org.apache.batik.test.TestException;
import org.apache.batik.test.TestReportProcessor;


import org.w3c.dom.Element;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This class can be used to build and run a <code>TestSuite</code> from
 * an XML description following the "XML Test Run" and "XML Test Suite"
 * formats, whose constants are defined in the <code>XTRunConstants</code>
 * and <code>XTSConstants</code> interfaces.
 *
 * This class takes a "Test Run" XML description as an input. That
 * description contains: <br>
 * + pointers to a number of "Test Suite" XML descriptions,
 * which contain the definition of the set of <code>Tests</code> to be
 * run and their configuration.<br>
 * + a description of the set of <code>TestReportProcessor</code> and
 * their configuration that should be used to process the reports
 * generated by the various <code>TestSuites</code>.<br>
 *
 * @author <a href="mailto:vhardy@apache.org">Vincent Hardy</a>
 * @version $Id$
 */
public class XMLTestSuiteRunner implements XTRunConstants, XTSConstants{
    /**
     * Displayed when no test or testSuite matching the input id was
     * found.
     * {0} : unmatched id set
     */
    public static final String MESSAGE_UNMATCHED_TEST_IDS
        = "XMLTestSuiteRunner.messages.unmatched.test.ids";

    /**
     * An error happened while processing a <code>TestreportProcessor</code>
     * description.
     * {0} : the &lt;testReportProcessor&gt; "className" attribute value
     * {1} : exception's class name
     * {2} : exception's message
     * {3} : exception's stack trace
     */
    public static final String CANNOT_CREATE_TEST_REPORT_PROCESSOR
        = "xml.XMLTestSuiteRunner.error.cannot.create.test.report.processor";

    /**
     * An error happened while running the <code>TestSuite</code>
     * {0} : <code>TestSuite</code> name
     * {1} : <code>TestSuite</code> class name.
     * {1} : exception's class name.
     * {2} : exception's message
     * {3} : exception's stack trace.
     */
    public static final String TEST_SUITE_EXCEPTION
        = "xml.XMLTestSuiteRunner.test.suite.exception";

    /**
     * An error happened while processing the <code>TestReport</code>
     * generated by the <code>TestSuite</code>
     * {0} : <code>TestReportProcessor</code> class name.
     * {1} : exception's class name.
     * {2} : exception's message
     * {3} : exception's stack trace.
     */
    public static final String TEST_REPORT_PROCESSING_EXCEPTION
        = "xml.XMLTestSuiteRunner.error.test.report.processing.exception";

    /**
     * Test filter which accepts all tests
     */
    public static class AcceptAllTestsFilter implements TestFilter{
        public Test filter(Test t){
            return t;
        }
    }

    /**
     * Test filter which only accepts tests with ids matching
     * the ones passed to its constructor.
     */
    public static class IdBasedTestFilter implements TestFilter {
        protected String[] ids;
        protected Set unmatchedIds = new HashSet();

        public IdBasedTestFilter(String[] ids){
            this.ids = ids;
            for (String id : ids) {
                unmatchedIds.add(id);
            }
        }

        public String traceUnusedIds(){
            Object[] ui = unmatchedIds.toArray();
            StringBuffer sb = null;
            if(ui != null && ui.length > 0){
                sb = new StringBuffer();
                sb.append(ui[0].toString());
                for(int i=1; i<ui.length; i++){
                    sb.append(", ");
                    sb.append(ui[i].toString());
                }
            }
            return sb != null ? sb.toString() : null;
        }

        /**
         * Remove children <code>Test</code> instances from the <code>TestSuite</code>
         * if they are filtered out.
         */
        public void filterTestSuite(TestSuite ts){
            Test[] t = ts.getChildrenTests();
            int nTests = t != null ? t.length : 0;
            for(int i=0; i<nTests; i++){
                if(filter(t[i]) == null){
                    ts.removeTest(t[i]);
                }
            }
        }

        /**
         * Accept a test if one of the ids is found (i.e., an
         * exact match or a substring) in the <code>Test</code>'s
         * qualified id.
         * <code>TestSuite</code>s are accepted if they have children and
         * rejected if they have none.
         */
        public Test filter(Test t){
            String id = t.getQualifiedId();
            boolean isRequested = isRequestedId(id);

            //
            // First, handle TestSuite children
            //
            if(t instanceof TestSuite){
                TestSuite ts = (TestSuite)t;
                filterTestSuite(ts);
                if(ts.getChildrenCount() > 0){
                    return t;
                }
                return null;
            }

            //
            // Now, handle leaf Tests
            //
            if(isRequested){
                return t;
            }

            return null;
        }

        protected boolean isRequestedId(String id){
            for (String id1 : ids) {
                //
                // id substring of ids[i]
                // ======================
                // if the test identifier (id) is a substring of one of the requested
                // then the test is one of the requested test parents and should be accepted.
                // Example: id = "all.B" with requested ids = "all.A all.B.B3"
                //          "all.B" (id) is a substring of "all.B.B3" (ids[1])
                // Conclusion: id is accepted because it is a parent test of ids[1]
                //
                // ids[i] substring of id
                // ======================
                // if one of the requested test identifiers (id[i]) is a substring of the
                // test id, then one of the test children is requested, so the test should
                // be accepted.
                // Example: id = "all.B.B3" with requested ids = "all.B"
                //          "all.B" (ids[0]) is a substring of "all.B.B3" (id)
                // Conclusion: id is accepted because it is a child test of ids[0]
                //
                if (id1.lastIndexOf(id) == 0) {
                    // System.out.println("accepting " + id + ". It is (or is the a parent of) " + ids[i]);
                    unmatchedIds.remove(id1);
                    return true;
                }

                if (id.lastIndexOf(id1) != -1) {
                    // System.out.println("accepting " + id + " it is (or is a child of) the requested " + ids[i]);
                    unmatchedIds.remove(id1);
                    return true;
                }
            }
            return false;
        }

    }

    /**
     * Builds an array of <code>TestReportProcessor</code> from the input
     * element, assuming the input element is a &lt;testSuite&gt; instance,
     */
    protected TestReportProcessor[] extractTestReportProcessor(Element element)
        throws TestException
    {
        List processors = new ArrayList();

        NodeList children = element.getChildNodes();
        if(children != null && children.getLength() > 0){
            int n = children.getLength();
            for(int i=0; i<n; i++){
                Node child = children.item(i);
                if(child.getNodeType() == Node.ELEMENT_NODE){
                    Element childElement = (Element)child;
                    String tagName = childElement.getTagName().intern();
                    if(tagName == XTRun_TEST_REPORT_PROCESSOR_TAG){
                        processors.add(buildProcessor(childElement));
                    }
                }
            }
        }

        TestReportProcessor[] p = null;
        if(processors.size() > 0){
            p = new TestReportProcessor[processors.size()];
            processors.toArray(p);
        }

        return p;
    }

    /**
     * Builds a <code>TestResultProcessor</code> from an element.
     */
    protected TestReportProcessor buildProcessor(Element element)
        throws TestException {
        try{
            return (TestReportProcessor)XMLReflect.buildObject(element);
        }catch(Exception e){
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            throw new TestException(CANNOT_CREATE_TEST_REPORT_PROCESSOR,
                                    new Object[] { element.getAttribute(XR_CLASS_ATTRIBUTE),
                                                   e.getClass().getName(),
                                                   e.getMessage(),
                                                   sw.toString() },
                                    e);
        }
    }

    /**
     * Builds a <code>TestSuite</code> from an input element.
     * This method assumes that element is a &lt;testRun&gt;
     * instance. The element is scanned for children
     * &lt;testSuite&gt; elements which is loaded into
     * a <code>Test</code> and composited into a <code>TestSuite</code>
     */
    protected DefaultTestSuite buildTestRunTestSuite(Element element)
        throws TestException {
        DefaultTestSuite testSuite = new DefaultTestSuite();

        //
        // Set the testRun name and id on the top level testSuite
        //
        String name = element.getAttribute(XTRun_NAME_ATTRIBUTE);
        testSuite.setName(name);

        String id = element.getAttribute(XTRun_ID_ATTRIBUTE);
        testSuite.setId(id);

        Element[] testSuites
            = getChildrenByTagName(element, XTRun_TEST_SUITE_TAG);

        int n = testSuites != null ? testSuites.length : 0;
        for(int i=0; i<n; i++){
            String suiteHref =
                testSuites[i].getAttribute(XTRun_HREF_ATTRIBUTE);

            Test test = XMLTestSuiteLoader.loadTestSuite(suiteHref, testSuite);
            if(test != null){
                testSuite.addTest(test);
            }
        }

        return testSuite;
    }

    /**
     * Gets all the children of a given type.
     */
    protected Element[] getChildrenByTagName(Element element,
                                             String tagName)
    {
        tagName = tagName.intern();
        List childrenWithTagName = new ArrayList();

        NodeList children = element.getChildNodes();
        if(children != null && children.getLength() > 0){
            int n = children.getLength();
            for(int i=0; i<n; i++){
                Node child = children.item(i);
                if(child.getNodeType() == Node.ELEMENT_NODE){
                    Element childElement = (Element)child;
                    String childTagName = childElement.getTagName().intern();
                    if(childTagName == tagName){
                        childrenWithTagName.add(childElement);
                    }
                }
            }
        }

        Element[] a = null;
        if(childrenWithTagName.size() > 0){
            a = new Element[childrenWithTagName.size()];
            childrenWithTagName.toArray(a);
        }

        return a;
    }


    /**
     * Runs the test suite described by the input
     * Document object. If the input ids array
     * is null or of zero length, then all the tests will be run.
     * Otherwise, only the tests identified by
     * the array will be run.
     */
    public TestReport run(Document doc, String[] ids)
        throws TestException {
        Element root = doc.getDocumentElement();

        return run(root, ids);
    }

    protected TestReport runTest(Test test)
        throws TestException {
        try{
            return test.run();
        }catch(Exception e){
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            throw new TestException(TEST_SUITE_EXCEPTION,
                                    new Object[] { test.getName(),
                                                   test.getClass().getName(),
                                                   e.getClass().getName(),
                                                   e.getMessage(),
                                                   sw.toString() },
                                    e);
        }
    }

    protected void processReport(TestReport report,
                                 TestReportProcessor[] processors)
        throws TestException {
        int n = processors.length;
        int i=0;
        try{
            for(; i<n; i++){
                processors[i].processReport(report);
            }
        }catch(Exception e){
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            throw new TestException(TEST_REPORT_PROCESSING_EXCEPTION,
                                    new Object[] { processors[i].getClass().getName(),
                                                   e.getClass().getName(),
                                                   e.getMessage(),
                                                   sw.toString() },
                                    e);
        }
    }

    protected TestReport run(Element testRunElement, String[] ids)
        throws TestException{
        //
        // First, build entire suite of tests
        //
        Test testRun
            = buildTestRunTestSuite(testRunElement);

        //
        // Filter testSuite if necessary
        //
        Test filteredTestRun = testRun;
        if(ids != null && ids.length > 0){
            IdBasedTestFilter filter = new IdBasedTestFilter(ids);
            filteredTestRun = filter.filter(testRun);
            String unusedIds = filter.traceUnusedIds();
            if(unusedIds != null){
                System.err.println(Messages.formatMessage(MESSAGE_UNMATCHED_TEST_IDS,
                                                          new Object[]{unusedIds}));
            }
        }

        if(filteredTestRun == null){
            DefaultTestReport report
                = new DefaultTestReport(testRun);
            report.setPassed(true);
            return report;
        }

        //
        // Now, get the set of TestReportProcessors
        // that can use the data
        //
        TestReportProcessor[] processors
            = extractTestReportProcessor(testRunElement);

        //
        // Run the test
        //
        TestReport report = runTest(testRun);

        //
        // Process the report
        //
        if(processors != null){
            processReport(report, processors);
        }

        return report;
    }

    /**
     * Displayed when the user passes no arguments to the command line.
     */
    public static final String USAGE
        = "XMLTestSuiteRunner.messages.error.usage";

    /**
     * Displayed when the input argument does not represent an existing
     * file to notify the user that the argument is going to be
     * interpreted as a URI.
     */
    public static final String NOT_A_FILE_TRY_URI
        = "XMLTestSuiteRunner.messages.error.not.a.file.try.uri";

    /**
     * Displayed when the input file name cannot be turned into a URL
     */
    public static final String COULD_NOT_CONVERT_FILE_NAME_TO_URI
        = "XMLTestSuiteRunner.messages.error.could.not.convert.file.name.to.uri";

    /**
     * Displayed when the input argument does not represent a valid
     * URI
     */
    public static final String INVALID_URI
        = "XMLTestSuiteRunner.messages.error.invalid.uri";

    /**
     * Displayed when the input document cannot be parsed.
     * {0} : uri of the invalid document.
     * {1} : exception generated while parsing
     * {2} : exception message
     */
    public static final String INVALID_DOCUMENT
        = "XMLTestSuiteRunner.messages.error.invalid.document";

    /**
     * Error displayed when an error occurs while running the
     * test suite
     */
    public static final String ERROR_RUNNING_TEST_SUITE
        = "XMLTestSuiteRunner.messages.error.running.test.suite";

    public static void main(String[] args) {
        if(args.length < 1){
            System.err.println(Messages.formatMessage(USAGE, null));
            System.exit(0);
        }

        String uriStr = args[0];
        String[] ids = new String[args.length - 1];
        System.arraycopy(args, 1, ids, 0, args.length-1);

        File file = new File(uriStr);
        URL url = null;
        if(file.exists()){
            try {
                url = file.toURI().toURL();
            }catch(MalformedURLException e){
                System.err.println(Messages.formatMessage(COULD_NOT_CONVERT_FILE_NAME_TO_URI,
                                                            new Object[]{uriStr}));
                System.exit(0);
            }
        }

        else {
            System.err.println(Messages.formatMessage(NOT_A_FILE_TRY_URI,
                                                        new Object[]{uriStr}));
            try{
                url = new URL(uriStr);
            }catch(MalformedURLException e){
                System.err.println(Messages.formatMessage(INVALID_URI,
                                                          new Object[]{uriStr}));
                System.exit(0);
            }
        }

        Document doc = null;

        try{
            System.err.println("Loading document ...");

            DocumentBuilder docBuilder
                = DocumentBuilderFactory.newInstance().newDocumentBuilder();

            doc = docBuilder.parse(url.toString());
        }catch(Exception e){
            e.printStackTrace();
            System.err.println(Messages.formatMessage(INVALID_DOCUMENT,
                                                      new Object[] { uriStr,
                                                                     e.getClass().getName(),
                                                                     e.getMessage() }));
            System.exit(0);
        }

        try{
            System.err.println("Running test run...");
            XMLTestSuiteRunner r = new XMLTestSuiteRunner();
            r.run(doc, ids);
        }catch(TestException e){
            System.err.println(Messages.formatMessage(ERROR_RUNNING_TEST_SUITE,
                                                      new Object[] { e.getMessage() }));
            System.exit(0);
        }

        System.exit(1);

    }
}
