/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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.ide.eclipse.adt.internal.sdk;

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import java.io.InputStream;
import java.io.StringReader;

import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Validator;

import junit.framework.TestCase;

/**
 * Tests local validation of a Layout-Devices sample XMLs using an XML Schema validator.
 */
public class TestLayoutDevicesXsd extends TestCase {

    @Override
    protected void setUp() throws Exception {
        super.setUp();
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    /**
     * A SAX error handler that captures the errors and warnings.
     * This allows us to capture *all* errors and just not get an exception on the first one.
     */
    private static class CaptureErrorHandler implements ErrorHandler {

        private String mWarnings = "";
        private String mErrors = "";

        @SuppressWarnings("unused")
        public String getErrors() {
            return mErrors;
        }

        @SuppressWarnings("unused")
        public String getWarnings() {
            return mWarnings;
        }

        /**
         * Verifies if the handler captures some errors or warnings.
         * Prints them on stderr.
         * Also fails the unit test if any error was generated.
         */
        public void verify() {
            if (mWarnings.length() > 0) {
                System.err.println(mWarnings);
            }

            if (mErrors.length() > 0) {
                System.err.println(mErrors);
                fail(mErrors);
            }
        }

        /**
         * @throws SAXException
         */
        public void error(SAXParseException ex) throws SAXException {
            mErrors += "Error: " + ex.getMessage() + "\n";
        }

        /**
         * @throws SAXException
         */
        public void fatalError(SAXParseException ex) throws SAXException {
            mErrors += "Fatal Error: " + ex.getMessage() + "\n";
        }

        /**
         * @throws SAXException
         */
        public void warning(SAXParseException ex) throws SAXException {
            mWarnings += "Warning: " + ex.getMessage() + "\n";
        }

    }

    // --- Helpers ------------

    /** An helper that validates a string against an expected regexp. */
    private void assertRegex(String expectedRegexp, String actualString) {
        assertNotNull(actualString);
        assertTrue(
                String.format("Regexp Assertion Failed:\nExpected: %s\nActual: %s\n",
                        expectedRegexp, actualString),
                actualString.matches(expectedRegexp));
    }

    public void checkFailure(String document, String regexp) throws Exception {
        Source source = new StreamSource(new StringReader(document));

        // don't capture the validator errors, we want it to fail and catch the exception
        Validator validator = LayoutDevicesXsd.getValidator(null);
        try {
            validator.validate(source);
        } catch (SAXParseException e) {
            // We expect a parse expression referring to this grammar rule
            assertRegex(regexp, e.getMessage());
            return;
        }
        // If we get here, the validator has not failed as we expected it to.
        fail();
    }

    public void checkSuccess(String document) throws Exception {
        Source source = new StreamSource(new StringReader(document));

        CaptureErrorHandler handler = new CaptureErrorHandler();
        Validator validator = LayoutDevicesXsd.getValidator(null);
        validator.validate(source);
        handler.verify();
    }

    // --- Tests ------------

    /** Validate a valid sample using an InputStream */
    public void testValidateLocalRepositoryFile() throws Exception {

        InputStream xmlStream =
            TestLayoutDevicesXsd.class.getResourceAsStream("config_sample.xml");
        Source source = new StreamSource(xmlStream);

        CaptureErrorHandler handler = new CaptureErrorHandler();
        Validator validator = LayoutDevicesXsd.getValidator(handler);
        validator.validate(source);
        handler.verify();
    }

    /** A document should at least have a root to be valid */
    public void testEmptyXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>",

                // expected failure
                "Premature end of file.*");
    }

    /** A document with an unknown element. */
    public void testUnknownContentXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:unknown />" +
                "</d:layout-devices>",

                // expected failure
                "cvc-complex-type.2.4.a: Invalid content was found.*");
    }

    /** A document with an missing attribute in a device element. */
    public void testIncompleteContentXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device />" +
                "</d:layout-devices>",

                // expected failure
                "cvc-complex-type.4: Attribute 'name' must appear on element 'd:device'.");
    }

    /** A document with a root element containing no device element is valid. */
    public void testEmptyRootXml() throws Exception {
        checkSuccess(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" />");
    }

    /** A document with an empty device element is not valid. */
    public void testEmptyDeviceXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device name=\"foo\"/>" +
                "</d:layout-devices>",

                // expected failure
                "cvc-complex-type.2.4.b: The content of element 'd:device' is not complete.*");
    }

    /** A document with two default elements in a device element is not valid. */
    public void testTwoDefaultsXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device name=\"foo\">" +
                "  <d:default />" +
                "  <d:default />" +
                "</d:device>" +
                "</d:layout-devices>",

                // expected failure
                "cvc-complex-type.2.4.a: Invalid content was found starting with element 'd:default'.*");
    }

    /** The default elements must be defined before the config one. It's invalid if after. */
    public void testDefaultConfigOrderXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device name=\"foo\">" +
                "  <d:config name=\"must-be-after-default\" />" +
                "  <d:default />" +
                "</d:device>" +
                "</d:layout-devices>",

                // expected failure
                "cvc-complex-type.2.4.a: Invalid content was found starting with element 'd:default'.*");
    }

    /** Screen dimension cannot be 0. */
    public void testScreenDimZeroXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device name=\"foo\">" +
                "  <d:default>" +
                "    <d:screen-dimension> <d:size>0</d:size> <d:size>1</d:size> </d:screen-dimension>" +
                "  </d:default>" +
                "</d:device>" +
                "</d:layout-devices>",

                // expected failure
                "cvc-minInclusive-valid: Value '0' is not facet-valid with respect to minInclusive '1'.*");
    }

    /** Screen dimension cannot be negative. */
    public void testScreenDimNegativeXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device name=\"foo\">" +
                "  <d:default>" +
                "    <d:screen-dimension> <d:size>-5</d:size> <d:size>1</d:size> </d:screen-dimension>" +
                "  </d:default>" +
                "</d:device>" +
                "</d:layout-devices>",

                // expected failure
                "cvc-minInclusive-valid: Value '-5' is not facet-valid with respect to minInclusive '1'.*");
    }

    /** X/Y dpi cannot be 0. */
    public void testXDpiZeroXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device name=\"foo\">" +
                "  <d:default>" +
                "    <d:xdpi>0</d:xdpi>" +
                "  </d:default>" +
                "</d:device>" +
                "</d:layout-devices>",

                // expected failure
                "cvc-minExclusive-valid: Value '0' is not facet-valid with respect to minExclusive '0.0E1'.*");
    }


    /** X/Y dpi cannot be negative. */
    public void testXDpiNegativeXml() throws Exception {
        checkFailure(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device name=\"foo\">" +
                "  <d:default>" +
                "    <d:xdpi>-3.1415926538</d:xdpi>" +
                "  </d:default>" +
                "</d:device>" +
                "</d:layout-devices>",

                // expected failure
                "cvc-minExclusive-valid: Value '-3.1415926538' is not facet-valid with respect to minExclusive '0.0E1'.*");
    }

    /** WHitespace around token is accepted by the schema. */
    public void testTokenWhitespaceXml() throws Exception {
        checkSuccess(
                // document
                "<?xml version=\"1.0\"?>" +
                "<d:layout-devices xmlns:d=\"http://schemas.android.com/sdk/android/layout-devices/1\" >" +
                "<d:device name=\"foo\">" +
                "  <d:config name='foo'>" +
                "    <d:screen-ratio>  \n long \r </d:screen-ratio>" +
                "  </d:config>" +
                "</d:device>" +
                "</d:layout-devices>");
    }

}

