/*
 * Copyright (C) 2011 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.utils;

import com.android.ide.common.blame.SourcePosition;

import junit.framework.TestCase;

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

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.OutputStreamWriter;
import java.io.Writer;

@SuppressWarnings({"javadoc", "IOResourceOpenedButNotSafelyClosed"})
public class PositionXmlParserTest extends TestCase {
    public void test() throws Exception {
        String xml =
                "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
                "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
                "    android:layout_width=\"match_parent\"\n" +
                "    android:layout_height=\"wrap_content\"\n" +
                "    android:orientation=\"vertical\" >\n" +
                "\n" +
                "    <Button\n" +
                "        android:id=\"@+id/button1\"\n" +
                "        android:layout_width=\"wrap_content\"\n" +
                "        android:layout_height=\"wrap_content\"\n" +
                "        android:text=\"Button\" />\n" +
                "\n" +
                "    <Button\n" +
                "        android:id=\"@+id/button2\"\n" +
                "        android:layout_width=\"wrap_content\"\n" +
                "        android:layout_height=\"wrap_content\"\n" +
                "        android:text=\"Button\" />\n" +
                "\n" +
                "</LinearLayout>\n";
        File file = File.createTempFile("parsertest", ".xml");
        Writer fw = new BufferedWriter(new FileWriter(file));
        fw.write(xml);
        fw.close();
        Document document = PositionXmlParser.parse(new FileInputStream(file));
        assertNotNull(document);

        // Basic parsing heart beat tests
        Element linearLayout = (Element) document.getElementsByTagName("LinearLayout").item(0);
        assertNotNull(linearLayout);
        NodeList buttons = document.getElementsByTagName("Button");
        assertEquals(2, buttons.getLength());
        final String ANDROID_URI = "http://schemas.android.com/apk/res/android";
        assertEquals("wrap_content",
                linearLayout.getAttributeNS(ANDROID_URI, "layout_height"));

        // Check attribute positions
        Attr attr = linearLayout.getAttributeNodeNS(ANDROID_URI, "layout_width");
        assertNotNull(attr);
        SourcePosition position = PositionXmlParser.getPosition(attr);
        assertEquals(2, position.getStartLine());
        assertEquals(4, position.getStartColumn());
        assertEquals(xml.indexOf("android:layout_width"), position.getStartOffset());
        assertEquals(2, position.getEndLine());
        String target = "android:layout_width=\"match_parent\"";
        assertEquals(xml.indexOf(target) + target.length(), position.getEndOffset());

        // Check element positions
        Element button = (Element) buttons.item(0);
        position = PositionXmlParser.getPosition(button);
        assertEquals(6, position.getStartLine());
        assertEquals(4, position.getStartColumn());
        assertEquals(xml.indexOf("<Button"), position.getStartOffset());
        assertEquals(xml.indexOf("/>") + 2, position.getEndOffset());
        assertEquals(10, position.getEndLine());
        int button1End = position.getEndOffset();

        Element button2 = (Element) buttons.item(1);
        position = PositionXmlParser.getPosition(button2);
        assertEquals(12, position.getStartLine());
        assertEquals(4, position.getStartColumn());
        assertEquals(xml.indexOf("<Button", button1End), position.getStartOffset());
        assertEquals(xml.indexOf("/>", position.getStartOffset()) + 2, position.getEndOffset());
        assertEquals(16, position.getEndLine());

        assertSame(button2, PositionXmlParser.findNodeAtLineAndCol(document,
                position.getStartLine(), position.getStartColumn()));
        assertSame(button2, PositionXmlParser.findNodeAtOffset(document,
                position.getStartOffset()));

        file.delete();
    }

    public static void testText() throws Exception {
        String xml =
                "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
                "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
                "    android:layout_width=\"match_parent\"\n" +
                "    android:layout_height=\"wrap_content\"\n" +
                "    android:orientation=\"vertical\" >\n" +
                "\n" +
                "    <Button\n" +
                "        android:id=\"@+id/button1\"\n" +
                "        android:layout_width=\"wrap_content\"\n" +
                "        android:layout_height=\"wrap_content\"\n" +
                "        android:text=\"Button\" />\n" +
                "          some text\n" +
                "\n" +
                "</LinearLayout>\n";
        File file = File.createTempFile("parsertest", ".xml");
        file.deleteOnExit();
        Writer fw = new BufferedWriter(new FileWriter(file));
        fw.write(xml);
        fw.close();
        Document document = PositionXmlParser.parse(new FileInputStream(file));
        assertNotNull(document);

        // Basic parsing heart beat tests
        Element linearLayout = (Element) document.getElementsByTagName("LinearLayout").item(0);
        assertNotNull(linearLayout);
        NodeList buttons = document.getElementsByTagName("Button");
        assertEquals(1, buttons.getLength());
        final String ANDROID_URI = "http://schemas.android.com/apk/res/android";
        assertEquals("wrap_content",
                linearLayout.getAttributeNS(ANDROID_URI, "layout_height"));

        // Check text positions
        Element button = (Element) buttons.item(0);
        Text text = (Text) button.getNextSibling();
        assertNotNull(text);

        // Check attribute positions
        SourcePosition start = PositionXmlParser.getPosition(text);
        assertEquals(11, start.getStartLine());
        assertEquals(10, start.getStartColumn());
        assertEquals(xml.indexOf("some text"), start.getStartOffset());

        // Check attribute positions with text node offsets
        start = PositionXmlParser.getPosition(text, 13, 15);
        assertEquals(11, start.getStartLine());
        assertEquals(12, start.getStartColumn());
        assertEquals(xml.indexOf("me"), start.getStartOffset());
    }

    public void testLineEndings() throws Exception {
        // Test for http://code.google.com/p/android/issues/detail?id=22925
        String xml =
                "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" +
                "<LinearLayout>\r\n" +
                "\r" +
                "<LinearLayout></LinearLayout>\r\n" +
                "</LinearLayout>\r\n";
        File file = File.createTempFile("parsertest2", ".xml");
        Writer fw = new BufferedWriter(new FileWriter(file));
        fw.write(xml);
        fw.close();
        Document document = PositionXmlParser.parse(new FileInputStream(file));
        assertNotNull(document);

        file.delete();
    }

    private static void checkEncoding(String encoding, boolean writeBom, boolean writeEncoding,
            String lineEnding)
            throws Exception {
        // Norwegian extra vowel characters such as "latin small letter a with ring above"
        String value = "\u00e6\u00d8\u00e5";
        StringBuilder sb = new StringBuilder();

        sb.append("<?xml version=\"1.0\"");
        if (writeEncoding) {
            sb.append(" encoding=\"");
            sb.append(encoding);
            sb.append("\"");
        }
        sb.append("?>");
        sb.append(lineEnding);
        sb.append(
                "<!-- This is a " + lineEnding +
                "     multiline comment" + lineEnding +
                "-->" + lineEnding +
                "<foo ");
        int startAttrOffset = sb.length();
        sb.append("attr=\"");
        sb.append(value);
        sb.append("\"");
        sb.append(">" + lineEnding +
                lineEnding +
                "<bar></bar>" + lineEnding +
                "</foo>" + lineEnding);
        File file = File.createTempFile("parsertest" + encoding + writeBom + writeEncoding,
                ".xml");
        BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(file));
        OutputStreamWriter writer = new OutputStreamWriter(stream, encoding);

        if (writeBom) {
            String normalized = encoding.toLowerCase().replace("-", "_");
            if (normalized.equals("utf_8")) {
                stream.write(0xef);
                stream.write(0xbb);
                stream.write(0xbf);
            } else if (normalized.equals("utf_16")) {
                stream.write(0xfe);
                stream.write(0xff);
            } else if (normalized.equals("utf_16le")) {
                stream.write(0xff);
                stream.write(0xfe);
            } else if (normalized.equals("utf_32")) {
                stream.write(0x0);
                stream.write(0x0);
                stream.write(0xfe);
                stream.write(0xff);
            } else if (normalized.equals("utf_32le")) {
                stream.write(0xff);
                stream.write(0xfe);
                stream.write(0x0);
                stream.write(0x0);
            } else {
                fail("Can't write BOM for encoding " + encoding);
            }
        }

        writer.write(sb.toString());
        writer.close();

        Document document = PositionXmlParser.parse(new FileInputStream(file));
        assertNotNull(document);
        Element root = document.getDocumentElement();
        assertEquals(file.getPath(), value, root.getAttribute("attr"));
        assertEquals(4, PositionXmlParser.getPosition(root).getStartLine());

        Attr attribute = root.getAttributeNode("attr");
        assertNotNull(attribute);
        SourcePosition position = PositionXmlParser.getPosition(attribute);
        assertNotNull(position);
        assertEquals(4, position.getStartLine());
        assertEquals(startAttrOffset, position.getStartOffset());

        file.delete();
    }

    public void testEncoding() throws Exception {
        checkEncoding("utf-8", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF-8", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF_16", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF-16", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF_16LE", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF_32", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF_32LE", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("windows-1252", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("MacRoman", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("ISO-8859-1", false /*bom*/, true /*encoding*/, "\n");
        checkEncoding("iso-8859-1", false /*bom*/, true /*encoding*/, "\n");

        // Try BOM's (with no encoding specified)
        checkEncoding("utf-8", true /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF-8", true /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF_16", true /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF-16", true /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF_16LE", true /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF_32", true /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF_32LE", true /*bom*/, false /*encoding*/, "\n");

        // Try default encodings (only defined for utf-8 and utf-16)
        checkEncoding("utf-8", false /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF-8", false /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF_16", false /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF-16", false /*bom*/, false /*encoding*/, "\n");
        checkEncoding("UTF_16LE", false /*bom*/, false /*encoding*/, "\n");

        // Try BOM's (with explicit encoding specified)
        checkEncoding("utf-8", true /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF-8", true /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF_16", true /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF-16", true /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF_16LE", true /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF_32", true /*bom*/, true /*encoding*/, "\n");
        checkEncoding("UTF_32LE", true /*bom*/, true /*encoding*/, "\n");

        // Make sure this works for \r and \r\n as well
        checkEncoding("UTF-16", false /*bom*/, true /*encoding*/, "\r");
        checkEncoding("UTF_16LE", false /*bom*/, true /*encoding*/, "\r");
        checkEncoding("UTF_32", false /*bom*/, true /*encoding*/, "\r");
        checkEncoding("UTF_32LE", false /*bom*/, true /*encoding*/, "\r");
        checkEncoding("windows-1252", false /*bom*/, true /*encoding*/, "\r");
        checkEncoding("MacRoman", false /*bom*/, true /*encoding*/, "\r");
        checkEncoding("ISO-8859-1", false /*bom*/, true /*encoding*/, "\r");
        checkEncoding("iso-8859-1", false /*bom*/, true /*encoding*/, "\r");

        checkEncoding("UTF-16", false /*bom*/, true /*encoding*/, "\r\n");
        checkEncoding("UTF_16LE", false /*bom*/, true /*encoding*/, "\r\n");
        checkEncoding("UTF_32", false /*bom*/, true /*encoding*/, "\r\n");
        checkEncoding("UTF_32LE", false /*bom*/, true /*encoding*/, "\r\n");
        checkEncoding("windows-1252", false /*bom*/, true /*encoding*/, "\r\n");
        checkEncoding("MacRoman", false /*bom*/, true /*encoding*/, "\r\n");
        checkEncoding("ISO-8859-1", false /*bom*/, true /*encoding*/, "\r\n");
        checkEncoding("iso-8859-1", false /*bom*/, true /*encoding*/, "\r\n");
    }

    public void testOneLineComment() throws Exception {
        String xml =
                "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
                        "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
                        "    android:layout_width=\"match_parent\"\n" +
                        "    android:layout_height=\"wrap_content\"\n" +
                        "    android:orientation=\"vertical\" >\n" +
                        "\n" +
                        "    <!-- this is a comment ! -->\n" +
                        "    <Button\n" +
                        "        android:id=\"@+id/button1\"\n" +
                        "        android:layout_width=\"wrap_content\"\n" +
                        "        android:layout_height=\"wrap_content\"\n" +
                        "        android:text=\"Button\" />\n" +
                        "          some text\n" +
                        "\n" +
                        "</LinearLayout>\n";
        File file = File.createTempFile("parsertest", ".xml");
        file.deleteOnExit();
        Writer fw = new BufferedWriter(new FileWriter(file));
        fw.write(xml);
        fw.close();
        Document document = PositionXmlParser.parse(new FileInputStream(file));
        assertNotNull(document);

        // Basic parsing heart beat tests
        Element linearLayout = (Element) document.getElementsByTagName("LinearLayout").item(0);
        assertNotNull(linearLayout);

        // first child is a comment.
        org.w3c.dom.Node commentNode = linearLayout.getFirstChild().getNextSibling();
        assertEquals(Node.COMMENT_NODE, commentNode.getNodeType());
        SourcePosition position = PositionXmlParser.getPosition(commentNode);
        assertNotNull(position);
        assertEquals(6, position.getStartLine());
        assertEquals(4, position.getStartColumn());
        assertEquals(xml.indexOf("<!--"), position.getStartOffset());

        // ensure that the next siblings' position start at the right location.
        Element button = (Element) document.getElementsByTagName("Button").item(0);
        SourcePosition buttonPosition = PositionXmlParser.getPosition(button);
        assertNotNull(buttonPosition);
        assertEquals(7, buttonPosition.getStartLine());
        assertEquals(4, buttonPosition.getStartColumn());
    }

    public void testMultipleLineComment() throws Exception {
        String xml =
                "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
                        "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
                        "    android:layout_width=\"match_parent\"\n" +
                        "    android:layout_height=\"wrap_content\"\n" +
                        "    android:orientation=\"vertical\" >\n" +
                        "\n" +
                        "    <!-- this is a more complicated \n" +
                        "         which spans on multiple lines\n" +
                        "         in the source xml -->\n" +
                        "    <Button\n" +
                        "        android:id=\"@+id/button1\"\n" +
                        "        android:layout_width=\"wrap_content\"\n" +
                        "        android:layout_height=\"wrap_content\"\n" +
                        "        android:text=\"Button\" />\n" +
                        "          some text\n" +
                        "\n" +
                        "</LinearLayout>\n";
        File file = File.createTempFile("parsertest", ".xml");
        file.deleteOnExit();
        Writer fw = new BufferedWriter(new FileWriter(file));
        fw.write(xml);
        fw.close();
        Document document = PositionXmlParser.parse(new FileInputStream(file));
        assertNotNull(document);

        // Basic parsing heart beat tests
        Element linearLayout = (Element) document.getElementsByTagName("LinearLayout").item(0);
        assertNotNull(linearLayout);

        // first child is a comment.
        Node commentNode = linearLayout.getFirstChild().getNextSibling();
        assertEquals(Node.COMMENT_NODE, commentNode.getNodeType());
        SourcePosition position = PositionXmlParser.getPosition(commentNode);
        assertNotNull(position);
        assertEquals(6, position.getStartLine());
        assertEquals(4, position.getStartColumn());


        // ensure that the next siblings' position start at the right location.
        Element button = (Element) document.getElementsByTagName("Button").item(0);
        SourcePosition buttonPosition = PositionXmlParser.getPosition(button);
        assertNotNull(buttonPosition);
        assertEquals(9, buttonPosition.getStartLine());
        assertEquals(4, buttonPosition.getStartColumn());
    }

    public void testAttributeWithoutNamespace() throws Exception {
        // Search for attribute with different prefixes and with no prefix.
        // Make sure we find it regardless of which one comes first (which is why we
        // list all 3 orders here and search for all attributes in all 3.)
        String xml = ""
                + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                + "<LinearLayout\n"
                + "        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
                + "        xmlns:other=\"http://foo.bar\">\n"
                + "    <Button\n"
                + "        android:id=\"@+id/button1\"\n"
                + "        android:orientation=\"vertical\"\n"
                + "        other:orientation=\"vertical\"\n"
                + "        orientation=\"true\"/>\n"
                + "    <Button\n"
                + "        android:id=\"@+id/button2\"\n"
                + "        other:orientation=\"vertical\"\n"
                + "        android:orientation=\"vertical\"\n"
                + "        orientation=\"true\"/>\n"
                + "    <Button\n"
                + "        android:id=\"@+id/button3\"\n"
                + "        orientation=\"true\"\n"
                + "        android:orientation=\"vertical\"\n"
                + "        other:orientation=\"vertical\"/>\n"
                + "</LinearLayout>\n"
                + "\n";

        File file = File.createTempFile("parsertest", ".xml");
        file.deleteOnExit();
        Writer fw = new BufferedWriter(new FileWriter(file));
        fw.write(xml);
        fw.close();
        Document document = PositionXmlParser.parse(new FileInputStream(file));
        assertNotNull(document);

        // Basic parsing heart beat tests
        Element linearLayout = (Element) document.getElementsByTagName("LinearLayout").item(0);
        assertNotNull(linearLayout);
        NodeList buttons = document.getElementsByTagName("Button");
        assertEquals(3, buttons.getLength());

        // Check attribute positions
        for (int i = 0, n = buttons.getLength(); i < n; i++) {
            Element button = (Element)buttons.item(i);
            Attr attr;
            SourcePosition position;

            attr = button.getAttributeNode("orientation");
            position = PositionXmlParser.getPosition(attr);
            assertNotNull(position);
            assertEquals(" orientation=\"true\"",
                    xml.substring(position.getStartOffset() - 1, position.getEndOffset()));

            attr = button.getAttributeNodeNS("http://schemas.android.com/apk/res/android",
                    "orientation");
            position = PositionXmlParser.getPosition(attr);
            assertNotNull(position);
            assertEquals("android:orientation=\"vertical\"",
                    xml.substring(position.getStartOffset(), position.getEndOffset()));

            attr = button.getAttributeNodeNS("http://foo.bar", "orientation");
            position = PositionXmlParser.getPosition(attr);
            assertNotNull(position);
            assertEquals("other:orientation=\"vertical\"",
                    xml.substring(position.getStartOffset(), position.getEndOffset()));
        }
    }


    public void testTagNamespace() throws Exception {

        final String NAMESPACE_URL = "http://example.org/path";
        final String XML =
                "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
                        "<ns:SomeTag xmlns:ns=\"" + NAMESPACE_URL + "\">\n" +
                        "    <ns:SubTag\n" +
                        "        ns:text=\"Button\" />\n" +
                        "</ns:SomeTag>\n";
        File file = File.createTempFile("parsertest", ".xml");
        Writer fw = new BufferedWriter(new FileWriter(file));
        fw.write(XML);
        fw.close();
        Document document = PositionXmlParser.parse(new FileInputStream(file));
        assertNotNull(document);
        Element e = document.getDocumentElement();
        assertNotNull(e);
        assertEquals("http://example.org/path", e.getNamespaceURI());
        assertEquals("ns:SomeTag", e.getTagName());
        assertEquals("ns:SomeTag", e.getNodeName());
        assertEquals("SomeTag",e.getLocalName());
        NodeList subTags = e.getElementsByTagNameNS(NAMESPACE_URL, "SubTag");
        assertEquals(1, subTags.getLength());
        Node subTagNode = subTags.item(0);
        assertEquals(Node.ELEMENT_NODE, subTagNode.getNodeType());
        Element subTag = (Element) subTagNode;
        assertEquals("ns:SubTag", subTag.getTagName());
        assertEquals("ns:SubTag", subTag.getNodeName());
        assertEquals("SubTag", subTag.getLocalName());
        Attr attr = subTag.getAttributeNodeNS(NAMESPACE_URL, "text");
        assertNotNull(attr);
        SourcePosition position = PositionXmlParser.getPosition(attr);
        assertNotNull(position);
        assertEquals("ns:text=\"Button\"",
                XML.substring(position.getStartOffset(), position.getEndOffset()));
        assertEquals("Button", subTag.getAttributeNS(NAMESPACE_URL, "text"));
        assertEquals(NAMESPACE_URL, subTag.getNamespaceURI());
    }
}
