/*
  This file is licensed 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 org.xmlunit.util;

import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import org.xmlunit.builder.Input;
import org.junit.Before;
import org.junit.Test;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
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.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;

import static org.junit.Assert.*;

public class NodesTest {

    private static final String FOO = "foo";
    private static final String BAR = "bar";
    private static final String SOME_URI = "urn:some:uri";

    private Document doc;

    @Before public void createDoc() throws Exception {
        doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
            .newDocument();
    }

    @Test public void qNameOfElementWithNoNs() {
        Element e = doc.createElement(FOO);
        QName q = Nodes.getQName(e);
        assertEquals(FOO, q.getLocalPart());
        assertEquals(XMLConstants.NULL_NS_URI, q.getNamespaceURI());
        assertEquals(XMLConstants.DEFAULT_NS_PREFIX, q.getPrefix());
        assertEquals(new QName(FOO), q);
    }

    @Test public void qNameOfElementWithNsNoPrefix() {
        Element e = doc.createElementNS(SOME_URI, FOO);
        QName q = Nodes.getQName(e);
        assertEquals(FOO, q.getLocalPart());
        assertEquals(SOME_URI, q.getNamespaceURI());
        assertEquals(XMLConstants.DEFAULT_NS_PREFIX, q.getPrefix());
        assertEquals(new QName(SOME_URI, FOO), q);
    }

    @Test public void qNameOfElementWithNsAndPrefix() {
        Element e = doc.createElementNS(SOME_URI, FOO);
        e.setPrefix(BAR);
        QName q = Nodes.getQName(e);
        assertEquals(FOO, q.getLocalPart());
        assertEquals(SOME_URI, q.getNamespaceURI());
        assertEquals(BAR, q.getPrefix());
        assertEquals(new QName(SOME_URI, FOO), q);
        assertEquals(new QName(SOME_URI, FOO, BAR), q);
    }

    @Test public void mergeNoTexts() {
        Element e = doc.createElement(FOO);
        assertEquals("", Nodes.getMergedNestedText(e));
    }

    @Test public void mergeSingleTextNode() {
        Element e = doc.createElement(FOO);
        Text t = doc.createTextNode(BAR);
        e.appendChild(t);
        assertEquals(BAR, Nodes.getMergedNestedText(e));
    }

    @Test public void mergeSingleCDATASection() {
        Element e = doc.createElement(FOO);
        CDATASection t = doc.createCDATASection(BAR);
        e.appendChild(t);
        assertEquals(BAR, Nodes.getMergedNestedText(e));
    }

    @Test public void mergeIgnoresTextOfChildren() {
        Element e = doc.createElement(FOO);
        Element c = doc.createElement("child");
        Text t = doc.createTextNode(BAR);
        e.appendChild(c);
        c.appendChild(t);
        assertEquals("", Nodes.getMergedNestedText(e));
    }

    @Test public void mergeIgnoresComments() {
        Element e = doc.createElement(FOO);
        Comment c = doc.createComment(BAR);
        e.appendChild(c);
        assertEquals("", Nodes.getMergedNestedText(e));
    }

    @Test public void mergeMultipleChildren() {
        Element e = doc.createElement(FOO);
        CDATASection c = doc.createCDATASection(BAR);
        e.appendChild(c);
        e.appendChild(doc.createElement("child"));
        Text t = doc.createTextNode(BAR);
        e.appendChild(t);
        assertEquals(BAR + BAR, Nodes.getMergedNestedText(e));
    }

    @Test public void attributeMapNoAttributes() {
        Element e = doc.createElement(FOO);
        Map<QName, String> m = Nodes.getAttributes(e);
        assertEquals(0, m.size());
    }

    @Test public void attributeMapNoNS() {
        Element e = doc.createElement(FOO);
        e.setAttribute(FOO, BAR);
        Map<QName, String> m = Nodes.getAttributes(e);
        assertEquals(1, m.size());
        assertEquals(BAR, m.get(new QName(FOO)));
    }

    @Test public void attributeMapwithNS() {
        Element e = doc.createElement(FOO);
        e.setAttributeNS(SOME_URI, FOO, BAR);
        Map<QName, String> m = Nodes.getAttributes(e);
        assertEquals(1, m.size());
        assertEquals(BAR, m.get(new QName(SOME_URI, FOO)));
        assertEquals(BAR, m.get(new QName(SOME_URI, FOO, BAR)));
    }

    @Test public void attributeMapWithFilter() {
        Element e = doc.createElement(FOO);
        e.setAttribute(FOO, BAR);
        e.setAttribute("x", "y");
        Map<QName, String> m = Nodes.getAttributes(e, new Predicate<Attr>() {
            @Override
            public boolean test(Attr a) {
                return FOO.equals(a.getName());
            }
        });
        assertEquals(1, m.size());
        assertEquals(BAR, m.get(new QName(FOO)));
    }

    private Document handleWsSetup() {
        return Convert.toDocument(Input.fromString(
                "<root>\n"
                        + "<!-- trim\tme -->\n"
                        + "<child attr=' trim me ' attr2='not me'>\n"
                        + " trim me \n"
                        + "</child><![CDATA[ trim me ]]>\n"
                        + "<?target  trim me ?>\n"
                        + "<![CDATA[          ]]>\n"
                        + "</root>").build());
    }

    private Map.Entry<Document, Node> stripWsSetup() {
        final Document toTest = handleWsSetup();
        final Node stripped = Nodes.stripWhitespace(toTest);
        return new Map.Entry<Document, Node>() {
            public Document getKey() {
                return toTest;
            }
            public Node getValue() {
                return stripped;
            }
            public Node setValue(Node n) {
                throw new UnsupportedOperationException();
            }
        };
    }

    private Map.Entry<Document, Node> normalizeWsSetup() {
        final Document toTest = handleWsSetup();
        final Node stripped = Nodes.normalizeWhitespace(toTest);
        return new Map.Entry<Document, Node>() {
            public Document getKey() {
                return toTest;
            }
            public Node getValue() {
                return stripped;
            }
            public Node setValue(Node n) {
                throw new UnsupportedOperationException();
            }
        };
    }

    @Test public void stripWhitespaceWorks() {
        handleWsWorks(stripWsSetup(), "trim\tme");
    }

    @Test public void normalizeWhitespaceWorks() {
        handleWsWorks(normalizeWsSetup(), "trim me");
    }

    private void handleWsWorks(Map.Entry<Document, Node> s,
                               String commentContent) {
        assertTrue(s.getValue() instanceof Document);
        NodeList top = s.getValue().getChildNodes();
        assertEquals(1, top.getLength());
        assertTrue(top.item(0) instanceof Element);
        assertEquals("root", top.item(0).getNodeName());
        NodeList rootsChildren = top.item(0).getChildNodes();
        assertEquals(4, rootsChildren.getLength());
        assertTrue("should be comment, is " + rootsChildren.item(0).getClass(),
                   rootsChildren.item(0) instanceof Comment);
        assertEquals(commentContent,
                     ((Comment) rootsChildren.item(0)).getData());
        assertTrue("should be element, is " + rootsChildren.item(1).getClass(),
                   rootsChildren.item(1) instanceof Element);
        assertEquals("child", rootsChildren.item(1).getNodeName());
        assertTrue("should be cdata, is " + rootsChildren.item(2).getClass(),
                   rootsChildren.item(2) instanceof CDATASection);
        assertEquals("trim me",
                     ((CDATASection) rootsChildren.item(2)).getData());
        assertTrue("should be PI, is " + rootsChildren.item(3).getClass(),
                   rootsChildren.item(3) instanceof ProcessingInstruction);
        assertEquals("trim me",
                     ((ProcessingInstruction) rootsChildren.item(3)).getData());
        Node child = rootsChildren.item(1);
        NodeList grandChildren = child.getChildNodes();
        assertEquals(1, grandChildren.getLength());
        assertTrue("should be text, is " + grandChildren.item(0).getClass(),
                   grandChildren.item(0) instanceof Text);
        assertEquals("trim me", ((Text) grandChildren.item(0)).getData());
        NamedNodeMap attrs = child.getAttributes();
        assertEquals(2, attrs.getLength());
        Attr a = (Attr) attrs.getNamedItem("attr");
        assertEquals("trim me", a.getValue());
        Attr a2 = (Attr) attrs.getNamedItem("attr2");
        assertEquals("not me", a2.getValue());
    }

    @Test public void stripWhitespaceDoesntAlterOriginal() {
        handleWsDoesntAlterOriginal(stripWsSetup());
    }

    @Test public void normalizeWhitespaceDoesntAlterOriginal() {
        handleWsDoesntAlterOriginal(normalizeWsSetup());
    }

    private void handleWsDoesntAlterOriginal(Map.Entry<Document, Node> s) {
        NodeList top = s.getKey().getChildNodes();
        assertEquals(1, top.getLength());
        assertTrue(top.item(0) instanceof Element);
        assertEquals("root", top.item(0).getNodeName());
        NodeList rootsChildren = top.item(0).getChildNodes();
        assertEquals(10, rootsChildren.getLength());
        assertNewlineTextNode(rootsChildren.item(0));
        assertTrue("should be comment, is " + rootsChildren.item(1).getClass(),
                   rootsChildren.item(1) instanceof Comment);
        assertEquals(" trim\tme ", ((Comment) rootsChildren.item(1)).getData());
        assertNewlineTextNode(rootsChildren.item(2));
        assertTrue("should be element, is " + rootsChildren.item(3).getClass(),
                   rootsChildren.item(3) instanceof Element);
        assertEquals("child", rootsChildren.item(3).getNodeName());
        assertTrue("should be cdata, is " + rootsChildren.item(4).getClass(),
                   rootsChildren.item(4) instanceof CDATASection);
        assertEquals(" trim me ",
                     ((CDATASection) rootsChildren.item(4)).getData());
        assertNewlineTextNode(rootsChildren.item(5));
        assertTrue("should be PI, is " + rootsChildren.item(6).getClass(),
                   rootsChildren.item(6) instanceof ProcessingInstruction);
        assertEquals("trim me ",
                     ((ProcessingInstruction) rootsChildren.item(6)).getData());
        assertNewlineTextNode(rootsChildren.item(7));
        assertTrue("should be cdata, is " + rootsChildren.item(8).getClass(),
                   rootsChildren.item(8) instanceof CDATASection);
        assertEquals("          ",
                     ((CDATASection) rootsChildren.item(8)).getData());
        assertNewlineTextNode(rootsChildren.item(9));
        Node child = rootsChildren.item(3);
        NodeList grandChildren = child.getChildNodes();
        assertEquals(1, grandChildren.getLength());
        assertTrue("should be text, is " + grandChildren.item(0).getClass(),
                   grandChildren.item(0) instanceof Text);
        assertEquals("\n trim me \n", ((Text) grandChildren.item(0)).getData());
        NamedNodeMap attrs = child.getAttributes();
        assertEquals(2, attrs.getLength());
        Attr a = (Attr) attrs.getNamedItem("attr");
        assertEquals(" trim me ", a.getValue());
        Attr a2 = (Attr) attrs.getNamedItem("attr2");
        assertEquals("not me", a2.getValue());
    }

    private static void assertNewlineTextNode(Node n) {
        assertTrue("should be text, is " + n.getClass(),
                   n instanceof Text);
        assertEquals("\n", ((Text) n).getData());
    }

    @Test public void normalize() {
        assertSame("foo", Nodes.normalize("foo"));
        assertSame("foo bar", Nodes.normalize("foo bar"));
        assertEquals("foo bar", Nodes.normalize("foo\nbar"));
        assertEquals("foo bar", Nodes.normalize("foo  \r\n\t bar"));
    }

    @Test
    public void stripECWWorks() {
        Node orig = handleWsSetup();
        Node s = Nodes.stripElementContentWhitespace(orig);

        assertTrue(s instanceof Document);
        NodeList top = s.getChildNodes();
        assertEquals(1, top.getLength());
        assertTrue(top.item(0) instanceof Element);
        assertEquals("root", top.item(0).getNodeName());
        NodeList rootsChildren = top.item(0).getChildNodes();
        assertEquals(4, rootsChildren.getLength());
        assertTrue("should be comment, is " + rootsChildren.item(0).getClass(),
                   rootsChildren.item(0) instanceof Comment);
        assertEquals(" trim\tme ",
                     ((Comment) rootsChildren.item(0)).getData());
        assertTrue("should be element, is " + rootsChildren.item(1).getClass(),
                   rootsChildren.item(1) instanceof Element);
        assertEquals("child", rootsChildren.item(1).getNodeName());
        assertTrue("should be cdata, is " + rootsChildren.item(2).getClass(),
                   rootsChildren.item(2) instanceof CDATASection);
        assertEquals(" trim me ",
                     ((CDATASection) rootsChildren.item(2)).getData());
        assertTrue("should be PI, is " + rootsChildren.item(3).getClass(),
                   rootsChildren.item(3) instanceof ProcessingInstruction);
        assertEquals("trim me ",
                     ((ProcessingInstruction) rootsChildren.item(3)).getData());
        Node child = rootsChildren.item(1);
        NodeList grandChildren = child.getChildNodes();
        assertEquals(1, grandChildren.getLength());
        assertTrue("should be text, is " + grandChildren.item(0).getClass(),
                   grandChildren.item(0) instanceof Text);
        assertEquals("\n trim me \n", ((Text) grandChildren.item(0)).getData());
        NamedNodeMap attrs = child.getAttributes();
        assertEquals(2, attrs.getLength());
        Attr a = (Attr) attrs.getNamedItem("attr");
        assertEquals(" trim me ", a.getValue());
        Attr a2 = (Attr) attrs.getNamedItem("attr2");
        assertEquals("not me", a2.getValue());
    }

    @Test
    public void getNodeListReturnsOriginalListForElements() {
        Element e = doc.createElement(FOO);
        NodeList directChildren = e.getChildNodes();
        NodeList mappedChildren = Nodes.getChildNodes(e);
        assertSame(directChildren, mappedChildren);
    }

    @Test
    public void getNodeListReturnsOriginalListForAttributesWithValues() {
        Element e = doc.createElement(FOO);
        e.setAttribute("a", "b");
        Node a = e.getAttributes().item(0);
        NodeList directChildren = a.getChildNodes();
        NodeList mappedChildren = Nodes.getChildNodes(a);
        assertSame(directChildren, mappedChildren);
    }

    @Test
    public void getNodeListReturnsNullSafeListForAttributesWithoutValues() {
        Element e = doc.createElement(FOO);
        e.setAttribute("a", null);
        Node a = e.getAttributes().item(0);
        NodeList directChildren = a.getChildNodes();
        NodeList mappedChildren = Nodes.getChildNodes(a);
        assertNotSame(directChildren, mappedChildren);

        assertEquals(1, directChildren.getLength());
        assertNull(directChildren.item(0));

        assertEquals(1, mappedChildren.getLength());
        assertNotNull(mappedChildren.item(0));
        assertTrue(mappedChildren.item(0) instanceof Text);
        assertEquals("", ((Text) mappedChildren.item(0)).getWholeText());
    }
}
