# -*- coding: utf-8 -*-

# This program is free software; you can redistribute it and/or modify it under
# the terms of the (LGPL) GNU Lesser General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# written by: Jurko Gospodnetić ( jurko.gospodnetic@pke.hr )

"""
CompareSAX testing utility unit tests.

"""

if __name__ == "__main__":
    import testutils
    testutils.run_using_pytest(globals())

import suds
import suds.sax.document
import suds.sax.parser
from testutils.assertion import assert_no_output
from testutils.compare_sax import CompareSAX

import pytest

import xml.sax


# CompareSAX class uses Python assertions to report failed comparison results
# so we need to skip the tests in this module if Python assertions have been
# disabled in the CompareSAX implementation module.
skip_test_if_CompareSAX_assertions_disabled = pytest.mark.skipif(
    not CompareSAX.assertions_enabled(),
    reason="CompareSAX assertions disabled")


@skip_test_if_CompareSAX_assertions_disabled
@pytest.mark.parametrize("data", (
    "",
    "<bad1/><bad2/>",
    '<bad a="1" a="1"/>',
    "<bad><bad>xml</document></bad>"))
def test_failed_parsing(data, capsys):
    pytest.raises(xml.sax.SAXParseException, CompareSAX.data2data, data, data)
    assert_no_output(capsys)


class TestMatched:
    """Successful CompareSAX matching tests."""

    @skip_test_if_CompareSAX_assertions_disabled
    def test_empty_document(self, capsys):
        a = suds.sax.document.Document()
        b = suds.sax.document.Document()
        CompareSAX.document2document(a, b)
        assert_no_output(capsys)

    @skip_test_if_CompareSAX_assertions_disabled
    @pytest.mark.parametrize(("data1", "data2"), (
        # Simple matches.
        ('<a><ns:b xmlns:ns="x"/></a>', '<a><ns:b xmlns:ns="x"/></a>'),
        ('<a><b xmlns="x"/></a>', '<a><b xmlns="x"/></a>'),
        ('<a xmlns="x"><b/></a>', '<a xmlns="x"><b/></a>'),
        # Extra namespace declarations.
        ('<ns1:b xmlns:ns1="two"/>', '<ns2:b xmlns="one" xmlns:ns2="two"/>'),
        ('<ns1:b xmlns:ns1="2"/>', '<ns2:b xmlns:ns3="1" xmlns:ns2="2"/>'),
        ('<b xmlns="1"/>', '<ns1:b xmlns="0" xmlns:ns1="1" xmlns:ns2="2"/>'),
        # Mismatched namespace prefixes.
        ('<a xmlns="one"/>', '<ns:a xmlns:ns="one"/>'),
        ('<ns1:b xmlns:ns1="two"/>', '<ns2:b xmlns:ns2="two"/>'),
        # Numeric unicode character references.
        ("<a>\u2606</a>", "<a>&#%d;</a>" % (0x2606,))))
    def test_data2data(self, data1, data2, capsys):
        CompareSAX.data2data(data1, data2)
        assert_no_output(capsys)

    @skip_test_if_CompareSAX_assertions_disabled
    @pytest.mark.parametrize("type1", (suds.byte_str, str))
    @pytest.mark.parametrize("type2", (suds.byte_str, str))
    def test_string_input_types(self, type1, type2, capsys):
        xml = "<a/>"
        CompareSAX.data2data(type1(xml), type2(xml))
        assert_no_output(capsys)

    @skip_test_if_CompareSAX_assertions_disabled
    def test_xml_encoding(self, capsys):
        """Test that the encoding listed in the XML declaration is honored."""
        xml_format = '<?xml version="1.0" encoding="%s"?><a>\u00D8</a>'
        data1 = (xml_format % ("UTF-8",)).encode('utf-8')
        data2 = (xml_format % ("latin1",)).encode('latin1')
        CompareSAX.data2data(data1, data2)
        assert_no_output(capsys)


class TestMismatched:
    """Failed CompareSAX matching tests."""

    @skip_test_if_CompareSAX_assertions_disabled
    @pytest.mark.parametrize(("data1", "data2", "expected_context"), (
        # Different element namespaces.
        ("<a/>", '<a xmlns="x"/>', "data2data.<a>.namespace"),
        ('<a xmlns="1"/>', '<a xmlns="2"/>', "data2data.<a>.namespace"),
        ('<r><a xmlns="1"/></r>', '<r><a xmlns="2"/></r>',
            "data2data.<r>.<a>.namespace"),
        ('<r><tag><a xmlns="1"/></tag><y/></r>',
            '<r><tag><a xmlns="2"/></tag><y/></r>',
            "data2data.<r>.<tag(1/2)>.<a>.namespace"),
        # Different textual content in text only nodes.
        ("<a>one</a>", "<a>two</a>", "data2data.<a>.text"),
        ("<a>x</a>", "<a>x </a>", "data2data.<a>.text"),
        ("<a>x</a>", "<a>x  </a>", "data2data.<a>.text"),
        ("<a>x </a>", "<a>x  </a>", "data2data.<a>.text"),
        ("<a> x</a>", "<a>x</a>", "data2data.<a>.text"),
        ("<a>  x</a>", "<a>x</a>", "data2data.<a>.text"),
        ("<a>  x</a>", "<a> x</a>", "data2data.<a>.text"),
        ("<a><b><c>x</c><c2/></b></a>", "<a><b><c>X</c><c2/></b></a>",
            "data2data.<a>.<b>.<c(1/2)>.text"),
        ("<a><b><c>x</c><d>y</d></b></a>", "<a><b><c>x</c><d>Y</d></b></a>",
            "data2data.<a>.<b>.<d(2/2)>.text"),
        # Different textual content in mixed content nodes with children.
        ("<a>42<b/><b/>42</a>", "<a>42<b/> <b/>42</a>", "data2data.<a>.text"),
        # Differently named elements.
        ("<a/>", "<b/>", "data2data.<a/b>"),
        ("<a><b/></a>", "<a><c/></a>", "data2data.<a>.<b/c>"),
        ("<a><b/><x/></a>", "<a><c/><x/></a>", "data2data.<a>.<b/c(1/2)>"),
        ("<a><x/><b/></a>", "<a><x/><c/></a>", "data2data.<a>.<b/c(2/2)>"),
        ("<a><b><c/></b></a>", "<a><b><d/></b></a>",
            "data2data.<a>.<b>.<c/d>"),
        ("<a><b><y1/><y2/><c/></b><x/></a>",
            "<a><b><y1/><y2/><d/></b><x/></a>",
            "data2data.<a>.<b(1/2)>.<c/d(3/3)>"),
        # Extra/missing non-root element.
        ("<a><b/></a>", "<a/>", "data2data.<a>"),
        ("<a/>", "<a><b/></a>", "data2data.<a>"),
        ("<a><x/><b/></a>", "<a><b/></a>", "data2data.<a>"),
        ("<a><b/><x/></a>", "<a><b/></a>", "data2data.<a>"),
        ("<a><b/></a>", "<a><x/><b/></a>", "data2data.<a>"),
        ("<a><b/></a>", "<a><b/><x/></a>", "data2data.<a>"),
        # Multiple differences.
        ("<a><b/></a>", "<c><d/></c>", "data2data.<a/c>"),
        ("<a><b/></a>", '<a xmlns="o"><c/></a>', "data2data.<a>.namespace"),
        ("<r><a><b/></a></r>", "<r><c><d/></c></r>", "data2data.<r>.<a/c>"),
        ("<r><a><b/></a></r>", '<r><a xmlns="o"><c/></a></r>',
            "data2data.<r>.<a>.namespace")))
    def test_data2data(self, data1, data2, expected_context, capsys):
        pytest.raises(AssertionError, CompareSAX.data2data, data1, data2)
        _assert_context_output(capsys, expected_context)

    @skip_test_if_CompareSAX_assertions_disabled
    def test_document2document_context(self, capsys):
        a = suds.sax.document.Document()
        b = suds.sax.parser.Parser().parse(string=suds.byte_str("<a/>"))
        pytest.raises(AssertionError, CompareSAX.document2document, a, b)
        _assert_context_output(capsys, "document2document")

    @skip_test_if_CompareSAX_assertions_disabled
    def test_document2element_context(self, capsys):
        a = suds.sax.parser.Parser().parse(string=suds.byte_str("<xx>1</xx>"))
        b = suds.sax.parser.Parser().parse(string=suds.byte_str("<xx>2</xx>"))
        pytest.raises(AssertionError, CompareSAX.document2element, a, b.root())
        _assert_context_output(capsys, "document2element.<xx>.text")

    @skip_test_if_CompareSAX_assertions_disabled
    def test_element2element_context(self, capsys):
        Parser = suds.sax.parser.Parser
        e1 = Parser().parse(string=suds.byte_str("<x/>")).root()
        e2 = Parser().parse(string=suds.byte_str("<y/>")).root()
        pytest.raises(AssertionError, CompareSAX.element2element, e1, e2)
        _assert_context_output(capsys, "element2element.<x/y>")

    @skip_test_if_CompareSAX_assertions_disabled
    def test_element2element_context_invalid_name__left(self, capsys):
        Parser = suds.sax.parser.Parser
        e = Parser().parse(string=suds.byte_str("<x/>")).root()
        e_invalid = object()
        pytest.raises(AssertionError, CompareSAX.element2element, e_invalid, e)
        _assert_context_output(capsys, "element2element.<???/x>")

    @skip_test_if_CompareSAX_assertions_disabled
    def test_element2element_context_invalid_name__right(self, capsys):
        Parser = suds.sax.parser.Parser
        e = Parser().parse(string=suds.byte_str("<y/>")).root()
        e_invalid = object()
        pytest.raises(AssertionError, CompareSAX.element2element, e, e_invalid)
        _assert_context_output(capsys, "element2element.<y/???>")

    @skip_test_if_CompareSAX_assertions_disabled
    def test_empty_vs_non_empty_document(self, capsys):
        document = suds.sax.document.Document()
        data = "<a/>"
        pytest.raises(AssertionError, CompareSAX.document2data, document, data)
        _assert_context_output(capsys, "document2data")


#TODO: TestSAXModelFeatures tests should be removed once their respective SAX
# document model features get tested by SAX document model specific unit tests.
#TODO: Additional missing suds SAX document model unit tests:
#  * SAX parser fails on documents with multiple root elements.
#  * SAX document may contain at most one element, accessible as root().
#  * SAX document append() overwrites the root element silently.
class TestSAXModelFeatures:
    """SAX document model feature testing using the CompareSAX interface."""

    @skip_test_if_CompareSAX_assertions_disabled
    @pytest.mark.parametrize(("data1", "data2"), (
        # Differently placed default namespace declaration.
        ('<ns:a xmlns:ns="1" xmlns="2"><b/></ns:a>',
        '<ns:a xmlns:ns="1"><b xmlns="2"/></ns:a>'),
        # Differently placed namespace prefix declaration.
        ('<a xmlns:ns="1"><ns:b/></a>', '<a><ns:b xmlns:ns="1"/></a>'),
        # Element's textual content merged.
        ("<a>111<b/>222</a>", "<a>111222<b/></a>"),
        ("<a>111<b/>222</a>", "<a><b/>111222</a>"),
        ("<a>111<b/>222</a>", "<a>11<b/>1222</a>"),
        # Explicit "" namespace == no prefix or default namespace.
        ('<a xmlns=""/>', "<a/>"),
        ('<ns:a xmlns:ns=""/>', "<a/>"),
        # Extra leading/trailing textual whitespace trimmed in mixed content
        # elements with more than one child element.
        ("<a>   \n\n <b/> \t\t\n\n</a>", "<a><b/></a>"),
        ("<a>   \nxxx\n <b/> \t\t\n\n</a>", "<a>xxx<b/></a>")))
    def test_data2data(self, data1, data2, capsys):
        CompareSAX.data2data(data1, data2)
        assert_no_output(capsys)


def _assert_context_output(capsys, context):
    """
    Test utility asserting an expected captured stderr context output and no
    captured stdout output.

    """
    out, err = capsys.readouterr()
    assert not out
    assert err == "Failed SAX XML comparison context:\n  %s\n" % (context,)
