/*
 * Copyright 2004 Sun Microsystems, Inc.
 *
 * 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.rometools.rome.io;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

public class XmlReaderTest {

    private static final String XML5 = "xml-prolog-encoding-spaced-single-quotes";
    private static final String XML4 = "xml-prolog-encoding-single-quotes";
    private static final String XML3 = "xml-prolog-encoding-double-quotes";
    private static final String XML2 = "xml-prolog";
    private static final String XML1 = "xml";

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    public static void main(final String[] args) throws Exception {
        final XmlReaderTest test = new XmlReaderTest();
        test.testRawBom();
        test.testRawNoBom();
        test.testHttp();
    }

    protected void testRawNoBomValid(final String encoding) throws Exception {
        checkEncoding(XML1, encoding, "UTF-8");
        checkEncoding(XML2, encoding, "UTF-8");
        checkEncoding(XML3, encoding, encoding);
        checkEncoding(XML4, encoding, encoding);
        checkEncoding(XML5, encoding, encoding);
    }

    private void checkEncoding(final String xmlType, final String encoding, final String expected) throws IOException {
        InputStream is = null;
        XmlReader xmlReader = null;
        try {
            is = getXmlStream("no-bom", xmlType, encoding, encoding);
            xmlReader = new XmlReader(is, false);
            assertEquals(xmlReader.getEncoding(), expected);
        } finally {
            if (xmlReader != null) {
                xmlReader.close();
            }
            if (is != null) {
                is.close();
            }
        }
    }

    protected void testRawNoBomInvalid(final String encoding) throws Exception {
        InputStream is = null;
        XmlReader xmlReader = null;
        try {
            is = getXmlStream("no-bom", XML3, encoding, encoding);
            xmlReader = new XmlReader(is, false);
            fail("It should have failed");
        } catch (final IOException ex) {
            assertTrue(ex.getMessage().indexOf("Invalid encoding,") > -1);
        } finally {
            if (xmlReader != null) {
                xmlReader.close();
            }
            if (is != null) {
                is.close();
            }
        }
    }

    @Test
    public void testRawNoBom() throws Exception {
        testRawNoBomValid("US-ASCII");
        testRawNoBomValid("UTF-8");
        testRawNoBomValid("ISO-8859-1");
        testRawNoBomValid("CP1047");
    }

    protected void testRawBomValid(final String encoding) throws Exception {
        final InputStream is = getXmlStream(encoding + "-bom", XML3, encoding, encoding);
        final XmlReader xmlReader = new XmlReader(is, false);
        if (!encoding.equals("UTF-16")) {
            assertEquals(xmlReader.getEncoding(), encoding);
        } else {
            assertEquals(xmlReader.getEncoding().substring(0, encoding.length()), encoding);
        }
        xmlReader.close();
    }

    protected void testRawBomInvalid(final String bomEnc, final String streamEnc, final String prologEnc) throws Exception {
        final InputStream is = getXmlStream(bomEnc, XML3, streamEnc, prologEnc);
        try {
            final XmlReader xmlReader = new XmlReader(is, false);
            fail("It should have failed for BOM " + bomEnc + ", streamEnc " + streamEnc + " and prologEnc " + prologEnc);
            xmlReader.close();
        } catch (final IOException ex) {
            assertTrue(ex.getMessage().indexOf("Invalid encoding,") > -1);
        }
    }

    @Test
    public void testRawBom() throws Exception {
        testRawBomValid("UTF-8");
        testRawBomValid("UTF-16BE");
        testRawBomValid("UTF-16LE");
        testRawBomValid("UTF-16");

        testRawBomInvalid("UTF-8-bom", "US-ASCII", "US-ASCII");
        testRawBomInvalid("UTF-8-bom", "ISO-8859-1", "ISO-8859-1");
        testRawBomInvalid("UTF-8-bom", "UTF-8", "UTF-16");
        testRawBomInvalid("UTF-8-bom", "UTF-8", "UTF-16BE");
        testRawBomInvalid("UTF-8-bom", "UTF-8", "UTF-16LE");
        testRawBomInvalid("UTF-16BE-bom", "UTF-16BE", "UTF-16LE");
        testRawBomInvalid("UTF-16LE-bom", "UTF-16LE", "UTF-16BE");
        testRawBomInvalid("UTF-16LE-bom", "UTF-16LE", "UTF-8");
    }

    @Test
    public void testHttp() throws Exception {
        testHttpValid("application/xml", "no-bom", "US-ASCII", null);
        testHttpValid("application/xml", "UTF-8-bom", "US-ASCII", null);
        testHttpValid("application/xml", "UTF-8-bom", "UTF-8", null);
        testHttpValid("application/xml", "UTF-8-bom", "UTF-8", "UTF-8");
        testHttpValid("application/xml;charset=UTF-8", "UTF-8-bom", "UTF-8", null);
        testHttpValid("application/xml;charset=\"UTF-8\"", "UTF-8-bom", "UTF-8", null);
        testHttpValid("application/xml;charset='UTF-8'", "UTF-8-bom", "UTF-8", null);
        testHttpValid("application/xml;charset=UTF-8", "UTF-8-bom", "UTF-8", "UTF-8");
        testHttpValid("application/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", null);
        testHttpValid("application/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", "UTF-16");
        testHttpValid("application/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", "UTF-16BE");

        testHttpInvalid("application/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", null);
        testHttpInvalid("application/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", "UTF-16");
        testHttpInvalid("application/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", "UTF-16BE");
        testHttpInvalid("application/xml", "UTF-8-bom", "US-ASCII", "US-ASCII");
        testHttpInvalid("application/xml;charset=UTF-16", "UTF-16LE", "UTF-8", "UTF-8");
        testHttpInvalid("application/xml;charset=UTF-16", "no-bom", "UTF-16BE", "UTF-16BE");

        testHttpValid("text/xml", "no-bom", "US-ASCII", null);
        testHttpValid("text/xml;charset=UTF-8", "UTF-8-bom", "UTF-8", "UTF-8");
        testHttpValid("text/xml;charset=UTF-8", "UTF-8-bom", "UTF-8", null);
        testHttpValid("text/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", null);
        testHttpValid("text/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", "UTF-16");
        testHttpValid("text/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", "UTF-16BE");
        testHttpValid("text/xml", "UTF-8-bom", "US-ASCII", null);

        testAlternateDefaultEncoding("application/xml", "UTF-8-bom", "UTF-8", null, null);
        testAlternateDefaultEncoding("application/xml", "no-bom", "US-ASCII", null, "US-ASCII");
        testAlternateDefaultEncoding("application/xml", "UTF-8-bom", "UTF-8", null, "UTF-8");
        testAlternateDefaultEncoding("text/xml", "no-bom", "US-ASCII", null, null);
        testAlternateDefaultEncoding("text/xml", "no-bom", "US-ASCII", null, "US-ASCII");
        testAlternateDefaultEncoding("text/xml", "no-bom", "US-ASCII", null, "UTF-8");

        testHttpInvalid("text/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", null);
        testHttpInvalid("text/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", "UTF-16");
        testHttpInvalid("text/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", "UTF-16BE");
        testHttpInvalid("text/xml;charset=UTF-16", "no-bom", "UTF-16BE", "UTF-16BE");
        testHttpInvalid("text/xml;charset=UTF-16", "no-bom", "UTF-16BE", null);

        testHttpLenient("text/xml", "no-bom", "US-ASCII", null, "US-ASCII");
        testHttpLenient("text/xml;charset=UTF-8", "UTF-8-bom", "UTF-8", "UTF-8", "UTF-8");
        testHttpLenient("text/xml;charset=UTF-8", "UTF-8-bom", "UTF-8", null, "UTF-8");
        testHttpLenient("text/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", null, "UTF-16BE");
        testHttpLenient("text/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", "UTF-16", "UTF-16");
        testHttpLenient("text/xml;charset=UTF-16", "UTF-16BE-bom", "UTF-16BE", "UTF-16BE", "UTF-16BE");
        testHttpLenient("text/xml", "UTF-8-bom", "US-ASCII", null, "US-ASCII");

        testHttpLenient("text/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", null, "UTF-16BE");
        testHttpLenient("text/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", "UTF-16", "UTF-16");
        testHttpLenient("text/xml;charset=UTF-16BE", "UTF-16BE-bom", "UTF-16BE", "UTF-16BE", "UTF-16BE");
        testHttpLenient("text/xml;charset=UTF-16", "no-bom", "UTF-16BE", "UTF-16BE", "UTF-16BE");
        testHttpLenient("text/xml;charset=UTF-16", "no-bom", "UTF-16BE", null, "UTF-16");

        testHttpLenient("text/html", "no-bom", "US-ASCII", "US-ASCII", "US-ASCII");
        testHttpLenient("text/html", "no-bom", "US-ASCII", null, "US-ASCII");
        testHttpLenient("text/html;charset=UTF-8", "no-bom", "US-ASCII", "UTF-8", "UTF-8");
        testHttpLenient("text/html;charset=UTF-16BE", "no-bom", "US-ASCII", "UTF-8", "UTF-8");
    }

    public void testAlternateDefaultEncoding(final String cT, final String bomEnc, final String streamEnc, final String prologEnc, final String alternateEnc)
            throws Exception {
        XmlReader xmlReader = null;
        try {
            final InputStream is;
            if (prologEnc == null) {
                is = getXmlStream(bomEnc, XML1, streamEnc, prologEnc);
            } else {
                is = getXmlStream(bomEnc, XML3, streamEnc, prologEnc);
            }
            XmlReader.setDefaultEncoding(alternateEnc);
            xmlReader = new XmlReader(is, cT, false);
            if (!streamEnc.equals("UTF-16")) {
                // we can not assert things here becuase UTF-8, US-ASCII and
                // ISO-8859-1 look alike for the chars used for detection
            } else {
                assertEquals(xmlReader.getEncoding().substring(0, streamEnc.length()), streamEnc);
            }
        } finally {
            XmlReader.setDefaultEncoding(null);
            if (xmlReader != null) {
                xmlReader.close();
            }
        }
    }

    public void testHttpValid(final String cT, final String bomEnc, final String streamEnc, final String prologEnc) throws Exception {
        final InputStream is;
        if (prologEnc == null) {
            is = getXmlStream(bomEnc, XML1, streamEnc, prologEnc);
        } else {
            is = getXmlStream(bomEnc, XML3, streamEnc, prologEnc);
        }
        final XmlReader xmlReader = new XmlReader(is, cT, false);
        if (!streamEnc.equals("UTF-16")) {
            // we can not assert things here becuase UTF-8, US-ASCII and
            // ISO-8859-1 look alike for the chars used for detection
        } else {
            assertEquals(xmlReader.getEncoding().substring(0, streamEnc.length()), streamEnc);
        }
        xmlReader.close();
    }

    protected void testHttpInvalid(final String cT, final String bomEnc, final String streamEnc, final String prologEnc) throws Exception {
        final InputStream is;
        if (prologEnc == null) {
            is = getXmlStream(bomEnc, XML2, streamEnc, prologEnc);
        } else {
            is = getXmlStream(bomEnc, XML3, streamEnc, prologEnc);
        }
        try {
            new XmlReader(is, cT, false);
            fail("It should have failed for HTTP Content-type " + cT + ", BOM " + bomEnc + ", streamEnc " + streamEnc + " and prologEnc " + prologEnc);
        } catch (final IOException ex) {
            assertTrue(ex.getMessage().indexOf("Invalid encoding,") > -1);
        }
    }

    protected void testHttpLenient(final String cT, final String bomEnc, final String streamEnc, final String prologEnc, final String shouldbe)
            throws Exception {
        final InputStream is;
        if (prologEnc == null) {
            is = getXmlStream(bomEnc, XML2, streamEnc, prologEnc);
        } else {
            is = getXmlStream(bomEnc, XML3, streamEnc, prologEnc);
        }
        final XmlReader xmlReader = new XmlReader(is, cT, true);
        assertEquals(xmlReader.getEncoding(), shouldbe);
        xmlReader.close();
    }

    private static final String ENCODING_ATTRIBUTE_XML = "<?xml version=\"1.0\" ?> \n" + "<atom:feed xmlns:atom=\"http://www.w3.org/2005/Atom\">\n" + "\n"
            + "  <atom:entry>\n" + "    <atom:title encoding=\"base64\"><![CDATA\n" + "aW5nTGluZSIgLz4";

    @Test
    public void testEncodingAttributeXML() throws Exception {
        final InputStream is = new ByteArrayInputStream(ENCODING_ATTRIBUTE_XML.getBytes());
        final XmlReader xmlReader = new XmlReader(is, "", true);
        assertEquals(xmlReader.getEncoding(), "UTF-8");
        xmlReader.close();
    }

    // XML Stream generator

    private static final int[] NO_BOM_BYTES = {};
    private static final int[] UTF_16BE_BOM_BYTES = { 0xFE, 0xFF };
    private static final int[] UTF_16LE_BOM_BYTES = { 0xFF, 0XFE };
    private static final int[] UTF_8_BOM_BYTES = { 0xEF, 0xBB, 0xBF };

    private static final Map<String, int[]> BOMs = new HashMap<String, int[]>();

    static {
        BOMs.put("no-bom", NO_BOM_BYTES);
        BOMs.put("UTF-16BE-bom", UTF_16BE_BOM_BYTES);
        BOMs.put("UTF-16LE-bom", UTF_16LE_BOM_BYTES);
        BOMs.put("UTF-16-bom", NO_BOM_BYTES); // it's added by the writer
        BOMs.put("UTF-8-bom", UTF_8_BOM_BYTES);
    }

    private static final MessageFormat XML = new MessageFormat("<root>{2}</root>");
    private static final MessageFormat XML_WITH_PROLOG = new MessageFormat("<?xml version=\"1.0\"?>\n<root>{2}</root>");
    private static final MessageFormat XML_WITH_PROLOG_AND_ENCODING_DOUBLE_QUOTES = new MessageFormat(
            "<?xml version=\"1.0\" encoding=\"{1}\"?>\n<root>{2}</root>");
    private static final MessageFormat XML_WITH_PROLOG_AND_ENCODING_SINGLE_QUOTES = new MessageFormat(
            "<?xml version=\"1.0\" encoding=''{1}''?>\n<root>{2}</root>");
    private static final MessageFormat XML_WITH_PROLOG_AND_ENCODING_SPACED_SINGLE_QUOTES = new MessageFormat(
            "<?xml version=\"1.0\" encoding =  \t \n \r''{1}''?>\n<root>{2}</root>");

    private static final MessageFormat INFO = new MessageFormat("\nBOM : {0}\nDoc : {1}\nStream Enc : {2}\nProlog Enc : {3}\n");

    private static final Map<String, MessageFormat> XMLs = new HashMap<String, MessageFormat>();

    static {
        XMLs.put(XML1, XML);
        XMLs.put(XML2, XML_WITH_PROLOG);
        XMLs.put(XML3, XML_WITH_PROLOG_AND_ENCODING_DOUBLE_QUOTES);
        XMLs.put(XML4, XML_WITH_PROLOG_AND_ENCODING_SINGLE_QUOTES);
        XMLs.put(XML5, XML_WITH_PROLOG_AND_ENCODING_SPACED_SINGLE_QUOTES);
    }

    /**
     *
     * @param bomType no-bom, UTF-16BE-bom, UTF-16LE-bom, UTF-8-bom
     * @param xmlType xml, xml-prolog, xml-prolog-charset
     * @return XML stream
     */
    protected InputStream getXmlStream(final String bomType, final String xmlType, final String streamEnc, final String prologEnc) throws IOException {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
        int[] bom = BOMs.get(bomType);
        if (bom == null) {
            bom = new int[0];
        }
        final MessageFormat xml = XMLs.get(xmlType);
        for (final int element : bom) {
            baos.write(element);
        }
        final Writer writer = new OutputStreamWriter(baos, streamEnc);
        final String info = INFO.format(new Object[] { bomType, xmlType, prologEnc });
        final String xmlDoc = xml.format(new Object[] { streamEnc, prologEnc, info });
        writer.write(xmlDoc);

        // PADDDING TO TEST THINGS WORK BEYOND PUSHBACK_SIZE
        writer.write("<da>\n");
        for (int i = 0; i < 10000; i++) {
            writer.write("<do/>\n");
        }
        writer.write("</da>\n");

        writer.close();
        return new ByteArrayInputStream(baos.toByteArray());
    }

    @Test
    public void testGetXmlProlog() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"TEST\"?>", "UTF-8");
        final String guessedEncoding = "UTF-8";

        final String prologEncoding = XmlReader.getXmlProlog(input, guessedEncoding);

        assertEquals("TEST", prologEncoding);
    }

    @Test
    public void testGetXmlProlog_Utf8() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"UTF-8\"?>", "UTF-8");
        final String guessedEncoding = "UTF-8";

        final String prologEncoding = XmlReader.getXmlProlog(input, guessedEncoding);

        assertEquals("UTF-8", prologEncoding);
    }

    @Test
    public void testGetXmlProlog_Utf16() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"UTF-16\"?>", "UTF-16");
        final String guessedEncoding = "UTF-16";

        assertEquals("UTF-16", XmlReader.getXmlProlog(input, guessedEncoding));
    }

    @Test
    public void testGetXmlProlog_Cp1047() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"CP1047\"?>", "CP1047");
        final String guessedEncoding = "CP1047";

        assertEquals("CP1047", XmlReader.getXmlProlog(input, guessedEncoding));
    }

    @Test
    public void testGetXmlProlog_NoEncoding() throws IOException {
        final InputStream input = stringToStream("<?xml>", "UTF-8");
        final String guessedEncoding = "UTF-8";

        assertNull(XmlReader.getXmlProlog(input, guessedEncoding));
    }

    @Test
    public void testGetXmlProlog_GuessedIsNull() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"UTF-8\"?>", "UTF-8");
        final String guessedEncoding = null;

        assertNull(XmlReader.getXmlProlog(input, guessedEncoding));
    }

    @Test
    public void testGetXmlProlog_UppercaseResult() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"utf-8\"?>", "UTF-8");
        final String guessedEncoding = "UTF-8";

        assertEquals("UTF-8", XmlReader.getXmlProlog(input, guessedEncoding));
    }

    @Test
    public void testGetXmlProlog_DifferentAsciiCompatible() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"TEST\"?>", "ISO-8859-1");
        final String guessedEncoding = "UTF-8";

        assertEquals("TEST", XmlReader.getXmlProlog(input, guessedEncoding));
    }

    @Test
    public void testGetXmlProlog_DifferentAsciiIncompatible() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"TEST\"?>", "UTF-16BE");
        final String guessedEncoding = "UTF-16LE";

        expectedException.expect(IOException.class);

        XmlReader.getXmlProlog(input, guessedEncoding);
    }

    @Test
    public void testGetXmlProlog_NoClosingAngleBracket() throws IOException {
        final InputStream input = stringToStream("<?xml encoding=\"TEST\"", "UTF-8");
        final String guessedEncoding = "UTF-8";

        expectedException.expect(IOException.class);

        XmlReader.getXmlProlog(input, guessedEncoding);
    }

    @Test
    public void testGetXmlProlog_Empty() throws IOException {
        final InputStream input = stringToStream("", "UTF-8");
        final String guessedEncoding = "UTF-8";

        expectedException.expect(IOException.class);

        XmlReader.getXmlProlog(input, guessedEncoding);
    }

    @Test
    public void testGetXmlProlog_ClosingAngleBracketIsTooFar() throws IOException {
        final StringBuilder spaces = new StringBuilder();
        for (int i = 0; i < 5000; i++) {
            spaces.append(" ");
        }

        final InputStream input = stringToStream("<?xml encoding=\"TEST\"?" + spaces + ">", "UTF-8");
        final String guessedEncoding = "UTF-8";

        expectedException.expect(IOException.class);

        XmlReader.getXmlProlog(input, guessedEncoding);
    }

    static InputStream stringToStream(String string, String encoding) {
        return new ByteArrayInputStream(string.getBytes(Charset.forName(encoding)));
    }
}
