/*
 * 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 freemarker.test.templatesuite;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

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

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import freemarker.ext.dom.NodeModel;
import freemarker.template.Configuration;
import freemarker.template.Version;
import freemarker.template.utility.StringUtil;
import junit.framework.TestSuite;

/**
 * Test suite where the test cases are defined in testcases.xml, and usually process
 * templates and compare their output with the expected output.
 * 
 * If you only want to run certain tests, you can specify a regular expression for
 * the test name in the {@link #TEST_FILTER_PROPERTY_NAME} system property.
 */
public class TemplateTestSuite extends TestSuite {
    
    private static final String ELEM_TEST_CASE = "testCase";

    private static final String ELEM_SETTING = "setting";

    private static final String ATTR_NO_OUTPUT = "noOutput";

    private static final String ATTR_EXPECTED = "expected";

    private static final String ATTR_TEMPLATE = "template";

    private static final String END_TEMPLATE_NAME_MARK = "[#endTN]";

    public static final String CONFIGURATION_XML_FILE_NAME = "testcases.xml";

    /**
     * When setting this system property, only the tests whose name matches the
     * given regular expression will be executed.
     */
    public static final String TEST_FILTER_PROPERTY_NAME = "freemareker.templateTestSuite.testFilter";
    
    /**
     * Comma separated list of "incompatible improvements" versions to run the test cases with.
     */
    public static final String INCOMPATIBLE_IMPROVEMENTS_PROPERTY_NAME
            = "freemareker.templateTestSuite.incompatibleImprovements";
    
    private final Map<String, String> testSuiteSettings = new LinkedHashMap<>();

    private final ArrayList<Version> testSuiteIcis;

    private final Pattern testCaseNameFilter;
    
    public static TestSuite suite() throws Exception {
        return new TemplateTestSuite();
    }
    
    public TemplateTestSuite() throws Exception {
        NodeModel.useJaxenXPathSupport();
        
        String filterStr = System.getProperty(TEST_FILTER_PROPERTY_NAME);
        testCaseNameFilter = filterStr != null ? Pattern.compile(filterStr) : null;
        if (testCaseNameFilter != null) {
            System.out.println("Note: " + TEST_FILTER_PROPERTY_NAME + " is " + StringUtil.jQuote(testCaseNameFilter));
        }
        
        testSuiteIcis = new ArrayList<>();
        String testedIcIsStr = System.getProperty(INCOMPATIBLE_IMPROVEMENTS_PROPERTY_NAME);
        if (testedIcIsStr != null) {
            for (String iciStr : testedIcIsStr.split(",")) {
                iciStr = iciStr.trim();
                if (iciStr.length() != 0) {
                    testSuiteIcis.add(new Version(iciStr));
                }
            }
        }
        if (testSuiteIcis.isEmpty()) {
            testSuiteIcis.add(getMinIcIVersion());
            testSuiteIcis.add(getMaxIcIVersion());
        }
        
        java.net.URL url = TemplateTestSuite.class.getResource(CONFIGURATION_XML_FILE_NAME);
        if (url == null) {
            throw new IOException("Resource not found: "
                    + TemplateTestSuite.class.getName() + ", " + CONFIGURATION_XML_FILE_NAME);
        }
        processConfigXML(url.toURI());
    }
    
    /**
     * Read the test case configurations file and build up the test suite.
     */
    public void processConfigXML(URI uri) throws Exception {
        Element testCasesElem = loadXMLFromURL(uri);
        
        NodeList children = testCasesElem.getChildNodes();
        for (int childIdx = 0; childIdx < children.getLength(); childIdx++) {
            Node n = children.item(childIdx);
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                final String nodeName = n.getNodeName();
                if (nodeName.equals(ELEM_SETTING)) {
                    NamedNodeMap attrs = n.getAttributes();
                    for (int attrIdx = 0; attrIdx < attrs.getLength(); attrIdx++) {
                        Attr attr = (Attr) attrs.item(attrIdx);
                        testSuiteSettings.put(attr.getName(), attr.getValue());
                    }
                } else if (nodeName.equals(ELEM_TEST_CASE)) {
                    for (TemplateTestCase testCase : createTestCasesFromElement((Element) n)) {
                        addTest(testCase);
                    }
                }
            }
        }
    }

    private Element loadXMLFromURL(URI uri) throws ParserConfigurationException, SAXException, IOException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        // dbf.setValidating(true);
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document d = db.parse(uri.toString());
        return d.getDocumentElement();
    }
    
    String getTextInElement(Element e) {
        StringBuilder buf = new StringBuilder();
        NodeList children = e.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node n = children.item(i);
            short type = n.getNodeType();
            if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) {
                buf.append(n.getNodeValue());
            }
        }
        return buf.toString();
    }
    
    /**
     * Returns the list of test cases generated from the {@link #ELEM_TEST_CASE} element.
     * There can be multiple generated test cases because of "incompatible improvements" variations, or none because
     * of the {@code nameFilter}.
     */
    private List<TemplateTestCase> createTestCasesFromElement(Element testCaseElem)
            throws Exception {
        final String caseName = StringUtil.emptyToNull(testCaseElem.getAttribute("name"));
        if (caseName == null) throw new Exception("Invalid XML: the \"name\" attribute is mandatory.");
        
        if (testCaseNameFilter != null
                && !testCaseNameFilter.matcher(caseName).matches()) {
            return Collections.emptyList();
        }
        
        final String templateName;
        final String expectedFileName;
        {
            final String beforeEndTN;
            final String afterEndTN;
            {
                int tBNameSep = caseName.indexOf(END_TEMPLATE_NAME_MARK);
                beforeEndTN = tBNameSep == -1 ? caseName : caseName.substring(0, tBNameSep);
                afterEndTN = tBNameSep == -1
                        ? "" : caseName.substring(tBNameSep + END_TEMPLATE_NAME_MARK.length());
            }
            
            {
                String s = StringUtil.emptyToNull(testCaseElem.getAttribute(ATTR_TEMPLATE));
                templateName = s != null ? s : beforeEndTN + ".ftl";
            }
    
            {
                String s = StringUtil.emptyToNull(testCaseElem.getAttribute(ATTR_EXPECTED));
                expectedFileName = s != null ? s : beforeEndTN + afterEndTN + ".txt";
            }
        }
        
        final boolean noOutput;
        {
            String s = StringUtil.emptyToNull(testCaseElem.getAttribute(ATTR_NO_OUTPUT));
            noOutput = s == null ? false : StringUtil.getYesNo(s);
        }

        final Map<String, String> testCaseSettings = getCaseFMSettings(testCaseElem);
        
        final List<Version> icisToTest;
        {
            final String testCaseIcis = testCaseSettings.get(Configuration.INCOMPATIBLE_IMPROVEMENTS) != null
                    ? testCaseSettings.get(Configuration.INCOMPATIBLE_IMPROVEMENTS)
                    : testCaseSettings.get(Configuration.INCOMPATIBLE_ENHANCEMENTS);
                    
            icisToTest = testCaseIcis != null ? parseVersionList(testCaseIcis) : testSuiteIcis;
            if (icisToTest.isEmpty()) {
                throw new Exception("The incompatible_improvement list was empty");
            }
        }

        List<TemplateTestCase> result = new ArrayList<>();
        for (Version iciToTest : icisToTest) {
            TemplateTestCase testCase = new TemplateTestCase(
                    caseName + "(ici=" + iciToTest + ")", caseName,
                    templateName, expectedFileName, noOutput, iciToTest);
            for (Map.Entry<String, String> setting : testSuiteSettings.entrySet()) {
                testCase.setSetting(setting.getKey(), setting.getValue());
            }
            for (Map.Entry<String, String> setting : testCaseSettings.entrySet()) {
                testCase.setSetting(setting.getKey(), setting.getValue());
            }
            
            result.add(testCase);
        }
        
        return result;
    }

    private List<Version> parseVersionList(String versionsStr) {
        List<Version> versions = new ArrayList<>();
        for (String versionStr : versionsStr.split(",")) {
            versionStr = versionStr.trim();
            if (versionStr.length() != 0) {
                final Version v;
                if ("min".equals(versionStr)) {
                    v = getMinIcIVersion();
                } else if ("max".equals(versionStr)) {
                    v = getMaxIcIVersion();
                } else {
                    v = new Version(versionStr);
                }
                if (!versions.contains(v)) {
                    versions.add(v);
                }
            }
        }
        return versions;
    }

    private Version getMaxIcIVersion() {
        Version v = Configuration.getVersion();
        // Remove nightly, RC and such:
        return new Version(v.getMajor(), v.getMinor(), v.getMicro());
    }

    private Version getMinIcIVersion() {
        return Configuration.VERSION_2_3_0;
    }

    private Map<String, String> getCaseFMSettings(Element e) {
        final Map<String, String> caseFMSettings;
        caseFMSettings = new LinkedHashMap<>();
        NodeList settingElems = e.getElementsByTagName(ELEM_SETTING);
        for (int elemIdx = 0; elemIdx < settingElems.getLength(); elemIdx++) {
            NamedNodeMap attrs = settingElems.item(elemIdx).getAttributes();
            for (int attrIdx = 0; attrIdx < attrs.getLength(); attrIdx++) {
                Attr attr = (Attr) attrs.item(attrIdx);

                final String settingName = attr.getName();
                caseFMSettings.put(settingName, attr.getValue());
            }
        }
        return caseFMSettings;
    }
    
    public static void main (String[] args) throws Exception {
        junit.textui.TestRunner.run(new TemplateTestSuite());
    }
    
}
