# -*- 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 )

"""
Unit tests related to Suds Python library reply processing.

Implemented using the 'pytest' testing framework.

"""

if __name__ == "__main__":
    from . import __init__
    __init__.runUsingPyTest(globals())


import suds
import tests

import pytest

import http.client
import re
import xml.sax


def test_ACCEPTED_and_NO_CONTENT_status_reported_as_None_with_faults():
    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
    f = lambda r, s : client.service.f(__inject={"reply":suds.byte_str(r),
        "status":s})
    assert f("", None) is None
    pytest.raises(Exception, f, "", http.client.INTERNAL_SERVER_ERROR)
    assert f("", http.client.ACCEPTED) is None
    assert f("", http.client.NO_CONTENT) is None
    assert f("bla-bla", http.client.ACCEPTED) is None
    assert f("bla-bla", http.client.NO_CONTENT) is None


def test_ACCEPTED_and_NO_CONTENT_status_reported_as_None_without_faults():
    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
    f = lambda r, s : client.service.f(__inject={"reply":suds.byte_str(r),
        "status":s})
    assert f("", None) is not None
    assert f("", http.client.INTERNAL_SERVER_ERROR) is not None
    assert f("", http.client.ACCEPTED) is None
    assert f("", http.client.NO_CONTENT) is None
    assert f("bla-bla", http.client.ACCEPTED) is None
    assert f("bla-bla", http.client.NO_CONTENT) is None


def test_badly_formed_reply_XML():
    for faults in (True, False):
        client = tests.client_from_wsdl(_wsdl__simple, faults=faults)
        pytest.raises(xml.sax.SAXParseException, client.service.f,
            __inject={"reply":suds.byte_str("bad food")})


# TODO: Update the current restriction type output parameter handling so such
# parameters get converted to the correct Python data type based on the
# restriction's underlying data type.
@pytest.mark.xfail
def test_restriction_data_types():
    client_unnamed = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="Elemento">
        <xsd:simpleType>
          <xsd:restriction base="xsd:int">
            <xsd:enumeration value="1" />
            <xsd:enumeration value="3" />
            <xsd:enumeration value="5" />
          </xsd:restriction>
        </xsd:simpleType>
      </xsd:element>""", "Elemento"))

    client_named = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:simpleType name="MyType">
        <xsd:restriction base="xsd:int">
          <xsd:enumeration value="1" />
          <xsd:enumeration value="3" />
          <xsd:enumeration value="5" />
        </xsd:restriction>
      </xsd:simpleType>
      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))

    client_twice_restricted = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:simpleType name="MyTypeGeneric">
        <xsd:restriction base="xsd:int">
          <xsd:enumeration value="1" />
          <xsd:enumeration value="2" />
          <xsd:enumeration value="3" />
          <xsd:enumeration value="4" />
          <xsd:enumeration value="5" />
        </xsd:restriction>
      </xsd:simpleType>
      <xsd:simpleType name="MyType">
        <xsd:restriction base="ns:MyTypeGeneric">
          <xsd:enumeration value="1" />
          <xsd:enumeration value="3" />
          <xsd:enumeration value="5" />
        </xsd:restriction>
      </xsd:simpleType>
      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))

    for client in (client_unnamed, client_named, client_twice_restricted):
        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <Elemento xmlns="my-namespace">5</Elemento>
  </env:Body>
</env:Envelope>""")))
        assert response.__class__ is int
        assert response == 5


def test_builtin_data_types(monkeypatch):
    monkeypatch.delitem(locals(), "e", False)

    integer_type_mapping = {
        "byte":int,
        "int":int,
        "integer":int,
        "long":int,
        "negativeInteger":int,
        "nonNegativeInteger":int,
        "nonPositiveInteger":int,
        "positiveInteger":int,
        "short":int,
        "unsignedByte":int,
        "unsignedInt":int,
        "unsignedLong":int,
        "unsignedShort":int}
    for tag, type in list(integer_type_mapping.items()):
        client = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="value" type="xsd:%s" />""" % tag, "value"))
        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <value xmlns="my-namespace">15</value>
  </env:Body>
</env:Envelope>""")))
        assert response.__class__ is type
        assert response == 15

    boolean_mapping = {"0":False, "1":True, "false":False, "true":True}
    client = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="value" type="xsd:boolean" />""", "value"))
    for value, expected_value in list(boolean_mapping.items()):
        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <value xmlns="my-namespace">%s</value>
  </env:Body>
</env:Envelope>""" % value)))
        assert response.__class__ is bool
        assert response == expected_value

    # Suds implements no extra range checking.
    client = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="value" type="xsd:byte" />""", "value"))
    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <value xmlns="my-namespace">1500</value>
  </env:Body>
</env:Envelope>""")))
    assert response.__class__ is int
    assert response == 1500

    #   Suds raises raw Python exceptions when it fails to convert received
    # response element data to its mapped Python integer data type, according
    # to the used WSDL schema.
    client = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="value" type="xsd:int" />""", "value"))
    e = pytest.raises(ValueError, client.service.f, __inject=dict(
        reply=suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <value xmlns="my-namespace">Fifteen</value>
  </env:Body>
</env:Envelope>"""))).value
    # ValueError instance received here has different string representations
    # depending on the Python version used:
    #   Python 2.4:
    #     "invalid literal for int(): Fifteen"
    #   Python 2.7.3, 3.2.3:
    #     "invalid literal for int() with base 10: 'Fifteen'"
    assert re.match("invalid literal for int\(\)( with base 10)?: ('?)Fifteen"
        "\\2$", str(e))

    # Suds returns invalid boolean values as None.
    invalid_boolean_values = ("True", "", "False", "2", "Fedora", "Z", "-1")
    client = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="value" type="xsd:boolean" />""", "value"))
    for value in invalid_boolean_values:
        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <value xmlns="my-namespace">%s</value>
  </env:Body>
</env:Envelope>""" % value)))
        assert response is None


def test_disabling_automated_simple_interface_unwrapping():
    client = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="Wrapper">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="Elemento" type="xsd:string" />
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>""", "Wrapper"), unwrap=False)
    assert not _isOutputWrapped(client, "f")

    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <Wrapper xmlns="my-namespace">
        <Elemento>La-di-da-da-da</Elemento>
    </Wrapper>
  </env:Body>
</env:Envelope>""")))

    assert response.__class__.__name__ == "Wrapper"
    assert len(response.__class__.__bases__) == 1
    assert response.__class__.__bases__[0] is suds.sudsobject.Object
    assert response.Elemento.__class__ is suds.sax.text.Text
    assert response.Elemento == "La-di-da-da-da"


def test_empty_reply():
    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
    f = lambda status=None, description=None : client.service.f(__inject=dict(
        reply=suds.byte_str(), status=status, description=description))
    status, reason = f()
    assert status == http.client.OK
    assert reason is None
    status, reason = f(http.client.OK)
    assert status == http.client.OK
    assert reason is None
    status, reason = f(http.client.INTERNAL_SERVER_ERROR)
    assert status == http.client.INTERNAL_SERVER_ERROR
    assert reason == 'injected reply'
    status, reason = f(http.client.FORBIDDEN)
    assert status == http.client.FORBIDDEN
    assert reason == 'injected reply'
    status, reason = f(http.client.FORBIDDEN, "kwack")
    assert status == http.client.FORBIDDEN
    assert reason == 'kwack'


def test_fault_reply_with_unicode_faultstring(monkeypatch):
    monkeypatch.delitem(locals(), "e", False)

    unicode_string = "€ Jurko Gospodnetić ČĆŽŠĐčćžšđ"
    fault_xml = suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <env:Fault>
      <faultcode>env:Client</faultcode>
      <faultstring>%s</faultstring>
    </env:Fault>
  </env:Body>
</env:Envelope>
""" % unicode_string)

    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
    inject = dict(reply=fault_xml, status=http.client.INTERNAL_SERVER_ERROR)
    e = pytest.raises(suds.WebFault, client.service.f, __inject=inject).value
    assert e.fault.faultstring == unicode_string
    assert e.document.__class__ is suds.sax.document.Document

    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
    status, fault = client.service.f(__inject=dict(reply=fault_xml,
        status=http.client.INTERNAL_SERVER_ERROR))
    assert status == http.client.INTERNAL_SERVER_ERROR
    assert fault.faultstring == unicode_string


def test_invalid_fault_namespace(monkeypatch):
    monkeypatch.delitem(locals(), "e", False)

    fault_xml = suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:p="x">
  <env:Body>
    <p:Fault>
      <faultcode>env:Client</faultcode>
      <faultstring>Dummy error.</faultstring>
      <detail>
        <errorcode>ultimate</errorcode>
      </detail>
    </p:Fault>
  </env:Body>
</env:Envelope>
""")
    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
    inject = dict(reply=fault_xml, status=http.client.OK)
    e = pytest.raises(Exception, client.service.f, __inject=inject).value
    assert e.__class__ is Exception
    assert str(e) == "<faultcode/> not mapped to message part"

    for http_status in (http.client.INTERNAL_SERVER_ERROR,
        http.client.PAYMENT_REQUIRED):
        status, reason = client.service.f(__inject=dict(reply=fault_xml,
            status=http_status, description="trla baba lan"))
        assert status == http_status
        assert reason == "trla baba lan"


def test_missing_wrapper_response():
    """
    Suds library's automatic structure unwrapping should not be applied to
    interpreting received SOAP Response XML.

    """
    client = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="Wrapper">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="fResponse" type="xsd:string" />
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>""", "Wrapper"))
    assert _isOutputWrapped(client, "f")

    response_with_missing_wrapper = client.service.f(__inject=dict(
        reply=suds.byte_str("""<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <fResponse xmlns="my-namespace">Anything</fResponse>
  </env:Body>
</env:Envelope>""")))
    assert response_with_missing_wrapper is None


def test_reply_error_with_detail_with_fault(monkeypatch):
    monkeypatch.delitem(locals(), "e", False)

    client = tests.client_from_wsdl(_wsdl__simple, faults=True)

    for http_status in (http.client.OK, http.client.INTERNAL_SERVER_ERROR):
        inject = dict(reply=_fault_reply__with_detail, status=http_status)
        e = pytest.raises(suds.WebFault, client.service.f, __inject=inject)
        e = e.value
        _test_fault(e.fault, True)
        assert e.document.__class__ is suds.sax.document.Document
        assert str(e) == "Server raised fault: 'Dummy error.'"

    inject = dict(reply=_fault_reply__with_detail, status=http.client.BAD_REQUEST,
        description="quack-quack")
    e = pytest.raises(Exception, client.service.f, __inject=inject).value
    assert e.__class__ is Exception
    assert e.args[0][0] == http.client.BAD_REQUEST
    assert e.args[0][1] == "quack-quack"


def test_reply_error_with_detail_without_fault():
    client = tests.client_from_wsdl(_wsdl__simple, faults=False)

    for http_status in (http.client.OK, http.client.INTERNAL_SERVER_ERROR):
        status, fault = client.service.f(__inject=dict(
            reply=_fault_reply__with_detail, status=http_status))
        assert status == http.client.INTERNAL_SERVER_ERROR
        _test_fault(fault, True)

    status, fault = client.service.f(__inject=dict(
        reply=_fault_reply__with_detail, status=http.client.BAD_REQUEST))
    assert status == http.client.BAD_REQUEST
    assert fault == "injected reply"

    status, fault = client.service.f(__inject=dict(
        reply=_fault_reply__with_detail, status=http.client.BAD_REQUEST,
        description="haleluja"))
    assert status == http.client.BAD_REQUEST
    assert fault == "haleluja"


def test_reply_error_without_detail_with_fault(monkeypatch):
    monkeypatch.delitem(locals(), "e", False)

    client = tests.client_from_wsdl(_wsdl__simple, faults=True)

    for http_status in (http.client.OK, http.client.INTERNAL_SERVER_ERROR):
        inject = dict(reply=_fault_reply__without_detail, status=http_status)
        e = pytest.raises(suds.WebFault, client.service.f, __inject=inject)
        e = e.value
        _test_fault(e.fault, False)
        assert e.document.__class__ is suds.sax.document.Document
        assert str(e) == "Server raised fault: 'Dummy error.'"

    inject = dict(reply=_fault_reply__with_detail, status=http.client.BAD_REQUEST,
        description="quack-quack")
    e = pytest.raises(Exception, client.service.f, __inject=inject).value
    assert e.__class__ is Exception
    assert e.args[0][0] == http.client.BAD_REQUEST
    assert e.args[0][1] == "quack-quack"


def test_reply_error_without_detail_without_fault():
    client = tests.client_from_wsdl(_wsdl__simple, faults=False)

    for http_status in (http.client.OK, http.client.INTERNAL_SERVER_ERROR):
        status, fault = client.service.f(__inject=dict(
            reply=_fault_reply__without_detail, status=http_status))
        assert status == http.client.INTERNAL_SERVER_ERROR
        _test_fault(fault, False)

    status, fault = client.service.f(__inject=dict(
        reply=_fault_reply__without_detail, status=http.client.BAD_REQUEST,
        description="kung-fu-fui"))
    assert status == http.client.BAD_REQUEST
    assert fault == "kung-fu-fui"


def test_simple_bare_and_wrapped_output():
    # Prepare web service proxies.
    client_bare = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="fResponse" type="xsd:string" />""", "fResponse"))
    client_wrapped = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="Wrapper">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="fResponse" type="xsd:string" />
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>""", "Wrapper"))

    #   Make sure suds library inteprets our WSDL definitions as wrapped or
    # bare output interfaces as expected.
    assert not _isOutputWrapped(client_bare, "f")
    assert _isOutputWrapped(client_wrapped, "f")

    #   Both bare & wrapped single parameter output web service operation
    # results get presented the same way even though the wrapped one actually
    # has an extra wrapper element around its received output data.
    data = "The meaning of life."
    get_response = lambda client, x : client.service.f(__inject=dict(
        reply=suds.byte_str(x)))

    response_bare = get_response(client_bare, """<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <fResponse xmlns="my-namespace">%s</fResponse>
  </env:Body>
</env:Envelope>""" % data)
    assert response_bare.__class__ is suds.sax.text.Text
    assert response_bare == data

    response_wrapped = get_response(client_wrapped, """<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <Wrapper xmlns="my-namespace">
      <fResponse>%s</fResponse>
    </Wrapper>
  </env:Body>
</env:Envelope>""" % data)
    assert response_wrapped.__class__ is suds.sax.text.Text
    assert response_wrapped == data


def test_wrapped_sequence_output():
    client = tests.client_from_wsdl(tests.wsdl_output("""\
      <xsd:element name="Wrapper">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="result1" type="xsd:string" />
            <xsd:element name="result2" type="xsd:string" />
            <xsd:element name="result3" type="xsd:string" />
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>""", "Wrapper"))
    assert _isOutputWrapped(client, "f")

    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <Wrapper xmlns="my-namespace">
        <result1>Uno</result1>
        <result2>Due</result2>
        <result3>Tre</result3>
    </Wrapper>
  </env:Body>
</env:Envelope>""")))

    #   Composite replies always get unmarshalled as a dynamically constructed
    # class named 'reply'.
    assert len(response.__class__.__bases__) == 1
    assert response.__class__.__name__ == "reply"
    assert response.__class__.__bases__[0] is suds.sudsobject.Object

    # Check response content.
    assert len(response) == 3
    assert response.result1 == "Uno"
    assert response.result2 == "Due"
    assert response.result3 == "Tre"
    assert response.result1.__class__ is suds.sax.text.Text
    assert response.result2.__class__ is suds.sax.text.Text
    assert response.result3.__class__ is suds.sax.text.Text


def _attibutes(object):
    result = set()
    for x in object:
        result.add(x[0])
    return result


def _isOutputWrapped(client, method_name):
    assert len(client.wsdl.bindings) == 1
    operation = list(client.wsdl.bindings.values())[0].operations[method_name]
    return operation.soap.output.body.wrapped


def _test_fault(fault, has_detail):
    assert fault.faultcode == "env:Client"
    assert fault.faultstring == "Dummy error."
    assert hasattr(fault, "detail") == has_detail
    assert not has_detail or fault.detail.errorcode == "ultimate"
    assert not hasattr(fault, "nonexisting")
    expected_attributes = set(("faultcode", "faultstring"))
    if has_detail:
        expected_attributes.add("detail")
    assert _attibutes(fault) == expected_attributes
    assert not has_detail or _attibutes(fault.detail) == set(("errorcode",))


_fault_reply__with_detail = suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <env:Fault>
      <faultcode>env:Client</faultcode>
      <faultstring>Dummy error.</faultstring>
      <detail>
        <errorcode>ultimate</errorcode>
      </detail>
    </env:Fault>
  </env:Body>
</env:Envelope>
""")

_fault_reply__without_detail = suds.byte_str("""\
<?xml version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <env:Fault>
      <faultcode>env:Client</faultcode>
      <faultstring>Dummy error.</faultstring>
    </env:Fault>
  </env:Body>
</env:Envelope>
""")

_wsdl__simple = tests.wsdl_output("""\
      <xsd:element name="fResponse">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="output_i" type="xsd:integer" />
            <xsd:element name="output_s" type="xsd:string" />
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>""", "fResponse")
