/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed 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 com.android.dvlib;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import junit.framework.TestCase;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

public class DeviceSchemaTest extends TestCase {

    //---- actual tests -----

    public void testValidXml_v1() throws Exception {
        InputStream xml = DeviceSchemaTest.class.getResourceAsStream("devices.xml");
        assertTrue(xml.markSupported());
        xml.mark(500000);   // set mark to beginning of stream

        // Check schema version
        assertEquals(1, DeviceSchema.getXmlSchemaVersion(xml));

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        boolean result = DeviceSchema.validate(xml, baos, null);
        String output = baos.toString().trim();
        assertTrue(
                String.format("Validation Assertion Failed, XML failed to validate when it was expected to pass\n%s\n", output),
                result);
        assertTrue(String.format(
                "Regex Assertion Failed\nExpected No Output\nActual: %s\n",
                baos.toString().trim()),
                baos.toString().trim().isEmpty());
    }

    public void testValidXml_v2() throws Exception {
        InputStream xml = DeviceSchemaTest.class.getResourceAsStream("devices_v2.xml");
        assertTrue(xml.markSupported());
        xml.mark(500000);   // set mark to beginning of stream

        // Check schema version
        assertEquals(2, DeviceSchema.getXmlSchemaVersion(xml));

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        boolean result = DeviceSchema.validate(xml, baos, null);
        String output = baos.toString().trim();
        assertTrue(
                String.format("Validation Assertion Failed, XML failed to validate when it was expected to pass\n%s\n", output),
                result);
        assertTrue(String.format(
                "Regex Assertion Failed\nExpected No Output\nActual: %s\n",
                baos.toString().trim()),
                baos.toString().trim().isEmpty());
    }

    public void testNoHardware() throws Exception {
        String regex = "Error: cvc-complex-type.2.4.a: Invalid content was found starting with "
                + "element 'd:software'.*";
        checkFailure("devices_no_hardware.xml", regex);
    }

    public void testNoSoftware() throws Exception {
        String regex = "Error: cvc-complex-type.2.4.a: Invalid content was found starting with "
                + "element 'd:state'.*";
        checkFailure("devices_no_software.xml", regex);
    }

    public void testNoDefault() throws Exception {
        String regex = "Error: No default state for device Galaxy Nexus.*";
        checkFailure("devices_no_default.xml", regex);
    }

    public void testTooManyDefaults() throws Exception {
        String regex = "Error: More than one default state for device Galaxy Nexus.*";
        checkFailure("devices_too_many_defaults.xml", regex);
    }

    public void testNoStates() throws Exception {
        String regex = "Error: cvc-complex-type.2.4.b: The content of element 'd:device' is not "
                + "complete.*\nError: No default state for device Galaxy Nexus.*";
        checkFailure("devices_no_states.xml", regex);
    }

    public void testBadMechanism() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_MECHANISM, "fanger");
        checkFailure(replacements, "Error: cvc-enumeration-valid: Value 'fanger' is not "
                + "facet-valid.*\nError: cvc-type.3.1.3: The value 'fanger' of element "
                + "'d:mechanism' is not valid.*");
    }

    public void testNegativeXdpi() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_XDPI, "-1.0");
        checkFailure(replacements, "Error: cvc-minInclusive-valid: Value '-1.0'.*\n"
                + "Error: cvc-type.3.1.3: The value '-1.0' of element 'd:xdpi' is not valid.*");
    }

    public void testNegativeYdpi() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_YDPI, "-1");
        checkFailure(replacements, "Error: cvc-minInclusive-valid: Value '-1'.*\n"
                + "Error: cvc-type.3.1.3: The value '-1' of element 'd:ydpi' is not valid.*");

    }

    public void testNegativeDiagonalLength() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_DIAGONAL_LENGTH, "-1.0");

        checkFailure(replacements, "Error: cvc-minInclusive-valid: Value '-1.0'.*\n"
                + "Error: cvc-type.3.1.3: The value '-1.0' of element 'd:diagonal-length'.*");
    }

    public void testInvalidOpenGLVersion() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_GL_VERSION, "2");
        checkFailure(replacements, "Error: cvc-pattern-valid: Value '2' is not facet-valid.*\n"
                + "Error: cvc-type.3.1.3: The value '2' of element 'd:gl-version' is not valid.*");
    }

    public void testEmptyOpenGLExtensions() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_GL_EXTENSIONS, "");
        checkSuccess(replacements);
    }

    public void testEmptySensors() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_SENSORS, "");
        checkSuccess(replacements);
    }

    public void testEmptyNetworking() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_NETWORKING, "");
        checkSuccess(replacements);
    }

    public void testEmptyCpu() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_CPU, "");
        checkFailure(replacements, "Error: cvc-minLength-valid: Value '' with length = '0'.*\n"
                + "Error: cvc-type.3.1.3: The value '' of element 'd:cpu' is not valid.*");
    }

    public void testEmptyGpu() throws Exception {
        Map<String, String> replacements = new HashMap<String, String>();
        replacements.put(DeviceSchema.NODE_GPU, "");
        checkFailure(replacements, "Error: cvc-minLength-valid: Value '' with length = '0'.*\n"
                + "Error: cvc-type.3.1.3: The value '' of element 'd:gpu' is not valid.*");
    }

    //---- helper methods -----

    private void checkFailure(Map<String, String> replacements, String regex) throws Exception {
        // Generate XML stream with replacements
        InputStream xmlStream = getReplacedStream(replacements);
        assertTrue(xmlStream.markSupported());
        xmlStream.mark(500000);   // set mark to beginning of stream

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        assertFalse(
                "Validation Assertion Failed, XML failed to validate when it was expected to pass\n",
                DeviceSchema.validate(xmlStream, baos, null));
        String actual = baos.toString().trim();
        actual = actual.replace("\r\n", "\n");  // Fix Windows CRLF
        assertTrue(
                String.format("Regex Assertion Failed:\nExpected: %s\nActual: %s\n", regex, actual),
                actual.matches(regex));
    }

    private void checkFailure(String resource, String regex) throws Exception {
        InputStream xml = DeviceSchemaTest.class.getResourceAsStream(resource);
        assertTrue(xml.markSupported());
        xml.mark(500000);   // set mark to beginning of stream
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        assertFalse("Validation Assertion Failed, XML validated when it was expected to fail\n",
                DeviceSchema.validate(xml, baos, null));

        String actual = baos.toString().trim();
        actual = actual.replace("\r\n", "\n");  // Fix Windows CRLF
        assertTrue(
                String.format("Regex Assertion Failed:\nExpected: %s\nActual: %s\n", regex, actual),
                actual.matches(regex));
    }

    private void checkSuccess(Map<String, String> replacements) throws Exception {
        InputStream xmlStream = getReplacedStream(replacements);
        assertTrue(xmlStream.markSupported());
        xmlStream.mark(500000);   // set mark to beginning of stream

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        assertTrue(DeviceSchema.validate(xmlStream, baos, null));
        assertTrue(baos.toString().trim().matches(""));
    }

    public static InputStream getReplacedStream(Map<String, String> replacements) throws Exception {
        InputStream xml = DeviceSchema.class.getResourceAsStream("devices_minimal.xml");
        SAXParserFactory factory = SAXParserFactory.newInstance();
        factory.setNamespaceAware(true);
        SAXParser parser = factory.newSAXParser();
        ReplacementHandler replacer = new ReplacementHandler(replacements);
        parser.parse(xml, replacer);
        Document doc = replacer.getGeneratedDocument();
        Transformer tf = TransformerFactory.newInstance().newTransformer();
        // Add indents so we're closer to user generated output
        tf.setOutputProperty(OutputKeys.INDENT, "yes");
        DOMSource source = new DOMSource(doc);
        StringWriter out = new StringWriter();
        StreamResult result = new StreamResult(out);
        tf.transform(source, result);
        return new ByteArrayInputStream(out.toString().getBytes("UTF-8"));
    }

    /**
     * Reads in a valid devices XML file and if an element tag is in the
     * replacements map, it replaces its text content with the corresponding
     * value. Note this has no concept of namespaces or hierarchy, so it will
     * replace the contents any and all elements with the specified tag name.
     */
    private static class ReplacementHandler extends DefaultHandler {
        private Element mCurrElement = null;
        private Document mDocument;
        private final Stack<Element> mElementStack = new Stack<Element>();
        private final Map<String, String> mPrefixes = new HashMap<String, String>();
        private final Map<String, String> mReplacements;
        private final StringBuilder mStringAccumulator = new StringBuilder();

        public ReplacementHandler(Map<String, String> replacements) {
            mReplacements = replacements;
        }

        @Override
        public void startDocument() {
            try {
                mDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            } catch (ParserConfigurationException e) {
                fail(e.getMessage());
            }
        }

        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes) {
            Element element = mDocument.createElement(name);
            for (int i = 0; i < attributes.getLength(); i++) {
                element.setAttribute(attributes.getQName(i), attributes.getValue(i));
            }
            for (String key : mPrefixes.keySet()) {
                element.setAttribute(XMLConstants.XMLNS_ATTRIBUTE + ":" + key, mPrefixes.get(key));
            }
            mPrefixes.clear();
            if (mCurrElement != null) {
                mElementStack.push(mCurrElement);
            }
            mCurrElement = element;
        }

        @Override
        public void startPrefixMapping(String prefix, String uri) throws SAXException {
            mPrefixes.put(prefix, uri);
        }

        @Override
        public void characters(char[] ch, int start, int length) {
            mStringAccumulator.append(ch, start, length);
        }

        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            if (mReplacements.containsKey(localName)) {
                mCurrElement.appendChild(mDocument.createTextNode(mReplacements.get(localName)));
            } else {
                String content = mStringAccumulator.toString().trim();
                if (!content.isEmpty()) {
                    mCurrElement.appendChild(mDocument.createTextNode(content));
                }
            }

            if (mElementStack.empty()) {
                mDocument.appendChild(mCurrElement);
                mCurrElement = null;
            } else {
                Element parent = mElementStack.pop();
                parent.appendChild(mCurrElement);
                mCurrElement = parent;
            }
            mStringAccumulator.setLength(0);
        }

        @Override
        public void error(SAXParseException e) {
            fail(e.getMessage());
        }

        @Override
        public void fatalError(SAXParseException e) {
            fail(e.getMessage());
        }

        public Document getGeneratedDocument() {
            return mDocument;
        }

    }
}
