# Copyright (C) 2021  Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import base64
import urllib.parse

import pytest

from passerelle.apps.soap.models import SOAPConnector

from . import utils

pytestmark = pytest.mark.django_db


class SOAP11:
    VERSION = '1.1'
    ENDPOINT_URL = 'https://www.examples.com/SayHello/'
    WSDL_CONTENT = '''\
<definitions name = "HelloService"
   targetNamespace = "http://www.examples.com/wsdl/HelloService.wsdl"
   xmlns = "http://schemas.xmlsoap.org/wsdl/"
   xmlns:soap = "http://schemas.xmlsoap.org/wsdl/soap/"
   xmlns:tns = "http://www.examples.com/wsdl/HelloService.wsdl"
   xmlns:xsd = "http://www.w3.org/2001/XMLSchema">

   <types>
     <schema targetNamespace="http://www.examples.com/wsdl/HelloService.wsdl" xmlns="http://www.w3.org/2001/XMLSchema">
      <element name="firstName">
        <complexType name="listofstring">
          <sequence>
            <element name="string" type="string" maxOccurs="unbounded"/>
          </sequence>
        </complexType>
      </element>
     </schema>
   </types>

   <message name = "SayHelloRequest">
      <part name = "firstName" element="tns:firstName"/>
      <part name = "lastName" type = "xsd:string"/>
   </message>

   <message name = "SayHelloResponse">
      <part name = "greeting" type = "xsd:string"/>
      <part name = "who" type = "xsd:string"/>
   </message>

   <portType name = "Hello_PortType">
      <operation name = "sayHello">
         <input message = "tns:SayHelloRequest"/>
         <output message = "tns:SayHelloResponse"/>
      </operation>
   </portType>

   <binding name = "Hello_Binding" type = "tns:Hello_PortType">
      <soap:binding style = "rpc"
         transport = "http://schemas.xmlsoap.org/soap/http"/>
      <operation name = "sayHello">
         <soap:operation soapAction = "sayHello"/>
         <input>
            <soap:body
               encodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
               namespace = "urn:examples:helloservice"
               use = "encoded"/>
         </input>

         <output>
            <soap:body
               encodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
               namespace = "urn:examples:helloservice"
               use = "encoded"/>
         </output>
      </operation>
   </binding>

   <service name = "Hello_Service">
      <documentation>WSDL File for HelloService</documentation>
      <port binding = "tns:Hello_Binding" name = "Hello_Port">
         <soap:address
            location = "http://www.examples.com/SayHello/" />
      </port>
   </service>
</definitions>'''
    WSDL_URL = 'https://example.com/service.wsdl'
    SOAP_RESPONSE = '''\
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
     <SayHelloResponse xmlns="urn:examples:helloservice">
       <greeting>Hello</greeting>
       <who>John!</who>
     </SayHelloResponse>
   </soap:Body>
</soap:Envelope>'''
    INPUT_SCHEMA = {
        'properties': {
            'firstName': {
                'description': '{http://www.examples.com/wsdl/HelloService.wsdl}firstName',
                'properties': {
                    'string': {'items': {'type': 'string', 'description': 'xsd:string'}, 'type': 'array'},
                },
                'required': ['string'],
                'type': 'object',
            },
            'lastName': {'type': 'string', 'description': 'xsd:string'},
        },
        'required': ['firstName', 'lastName'],
        'type': 'object',
    }
    OUTPUT_SCHEMA = {
        'properties': {
            'greeting': {'type': 'string', 'description': 'xsd:string'},
            'who': {'type': 'string', 'description': 'xsd:string'},
        },
        'required': ['greeting', 'who'],
        'type': 'object',
    }
    INPUT_DATA = {
        'firstName/string/0': 'John',
        'firstName/string/1': 'Bill',
        'lastName': 'Doe',
    }
    OUTPUT_DATA = {
        'greeting': 'Hello',
        'who': 'John!',
    }
    VALIDATION_ERROR = 'Missing element firstName (sayHello.firstName)'


class SOAP12(SOAP11):
    VERSION = '1.2'
    ENDPOINT_URL = 'https://www.examples.com/SayHello/'
    WSDL_CONTENT = f'''\
<?xml version="1.0"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
  xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
  xmlns:tns="urn:examples:helloservice"
  targetNamespace="urn:examples:helloservice">

  <wsdl:types>
    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
      xmlns:tns="urn:examples:helloservice"
      targetNamespace="urn:examples:helloservice">
      <xsd:element name="sayHello">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="firstName" type="xsd:string" maxOccurs="unbounded"/>
            <xsd:element name="lastName" type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="sayHelloResponse">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="greeting" type="xsd:string"/>
            <xsd:element name="who" type="xsd:string" maxOccurs="unbounded"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>
  </wsdl:types>

  <wsdl:message name="sayHello">
    <wsdl:part name="sayHelloInputPart" element="tns:sayHello"/>
  </wsdl:message>
  <wsdl:message name="sayHelloResponse">
    <wsdl:part name="sayHelloOutputPart" element="tns:sayHelloResponse"/>
  </wsdl:message>

  <wsdl:portType name="sayHelloPortType">
    <wsdl:operation name="sayHello">
      <wsdl:input name="sayHello" message="tns:sayHello"/>
      <wsdl:output name="sayHelloResponse" message="tns:sayHelloResponse"/>
    </wsdl:operation>
  </wsdl:portType>

  <wsdl:binding name="sayHelloBinding"
    type="tns:sayHelloPortType">
    <soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="sayHello">
      <soap12:operation style="document"/>
      <wsdl:input name="sayHello">
        <soap12:body use="literal"/>
      </wsdl:input>
      <wsdl:output name="sayHelloResponse">
        <soap12:body use="literal"/>
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>

  <wsdl:service name="sayHelloService">
    <wsdl:port name="sayHelloPort"
      binding="tns:sayHelloBinding">
      <soap12:address location="{ENDPOINT_URL}"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>'''
    SOAP_RESPONSE = '''\
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
  xmlns:ser="http://www.herongyang.com/Service/">
  <soap:Header/>
  <soap:Body>
     <sayHelloResponse xmlns="urn:examples:helloservice">
       <greeting>Hello</greeting>
       <who>John!</who>
     </sayHelloResponse>
  </soap:Body>
</soap:Envelope>'''
    INPUT_SCHEMA = {
        'type': 'object',
        'properties': {
            'firstName': {'type': 'array', 'items': {'type': 'string', 'description': 'xsd:string'}},
            'lastName': {'type': 'string', 'description': 'xsd:string'},
        },
        'required': ['firstName', 'lastName'],
        'description': '{urn:examples:helloservice}sayHello',
    }
    OUTPUT_SCHEMA = {
        'description': '{urn:examples:helloservice}sayHelloResponse',
        'properties': {
            'greeting': {'type': 'string', 'description': 'xsd:string'},
            'who': {
                'type': 'array',
                'items': {'type': 'string', 'description': 'xsd:string'},
            },
        },
        'required': ['greeting', 'who'],
        'type': 'object',
    }
    INPUT_DATA = {
        'firstName/0': 'John',
        'firstName/1': 'Bill',
        'lastName': 'Doe',
    }
    OUTPUT_DATA = {
        'greeting': 'Hello',
        'who': ['John!'],
    }
    VALIDATION_ERROR = 'Expected at least 1 items (minOccurs check) 0 items found. (sayHello.firstName)'


class BrokenSOAP12(SOAP12):
    WSDL_CONTENT = SOAP12.WSDL_CONTENT[-100:]  # truncate the WSDL to break it


@pytest.fixture(params=[SOAP11, SOAP12])
def soap(request):
    p = request.param()
    with utils.mock_url(p.WSDL_URL, response=p.WSDL_CONTENT):
        with utils.mock_url(p.ENDPOINT_URL, response=p.SOAP_RESPONSE) as mock:
            p.endpoint_mock = mock
            yield p


class TestManage:
    @pytest.fixture
    def app(self, app, admin_user):
        from .test_manager import login

        login(app)
        return app

    def test_homepage(self, app, connector, soap):
        response = app.get(f'/soap/{connector.slug}/')
        assert 'Method sayHello' in response

    @pytest.mark.parametrize('soap', [BrokenSOAP12], indirect=True)
    def test_homepage_broken_wsdl(self, app, connector, soap):
        response = app.get(f'/soap/{connector.slug}/')
        response = app.get(f'/soap/{connector.slug}/')
        assert response.pyquery('.down')


@pytest.fixture
def connector(db, soap):
    return utils.setup_access_rights(
        SOAPConnector.objects.create(
            slug='test', wsdl_url=soap.WSDL_URL, zeep_strict=True, zeep_xsd_ignore_sequence_order=False
        )
    )


def test_schemas(connector, soap):
    assert list(connector.operations_and_schemas) == [('sayHello', soap.INPUT_SCHEMA, soap.OUTPUT_SCHEMA)]


def test_say_hello_method_validation_error(connector, soap, app):
    resp = app.get('/soap/test/method/sayHello/')
    assert resp.json == {
        "err": 1,
        "err_class": "passerelle.utils.soap.SOAPError",
        "err_desc": soap.VALIDATION_ERROR,
        "data": None,
    }


def test_say_hello_method_ok_get(connector, app, caplog, soap):
    resp = app.get('/soap/test/method/sayHello/?' + urllib.parse.urlencode(soap.INPUT_DATA))
    assert '>John<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode()
    assert '>Bill<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode()

    assert '>Doe<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode()
    assert resp.json == {'data': soap.OUTPUT_DATA, 'err': 0}


def test_say_hello_method_ok_get_with_signature(connector, app, caplog, soap):
    resp = app.get('/soap/test/method/sayHello/?' + urllib.parse.urlencode(soap.INPUT_DATA) + '&signature=1')
    assert resp.json['err'] == 0


def test_say_hello_method_ok_post_json(connector, app, caplog, soap):
    resp = app.post_json('/soap/test/method/sayHello/', params=soap.INPUT_DATA)
    assert '>John<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode()
    assert '>Bill<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode()
    assert '>Doe<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode()
    assert resp.json == {'data': soap.OUTPUT_DATA, 'err': 0}


@pytest.mark.parametrize('soap', [SOAP12], indirect=True)
class TestAuthencation:
    def test_basic_auth(self, connector, app, caplog, soap):
        connector.basic_auth_username = 'username'
        connector.basic_auth_password = 'password'
        connector.save()

        app.post_json('/soap/test/method/sayHello/', params=soap.INPUT_DATA)
        assert (
            base64.b64decode(
                soap.endpoint_mock.handlers[0].call['requests'][1].headers['Authorization'].split()[1]
            )
            == b'username:password'
        )
        assert b'wsse:UsernameToken' not in soap.endpoint_mock.handlers[0].call['requests'][1].body

    def test_username_token(self, connector, app, caplog, soap):
        connector.zeep_wsse_username = 'username'
        connector.zeep_wsse_password = 'password'
        connector.save()

        app.post_json('/soap/test/method/sayHello/', params=soap.INPUT_DATA)
        assert 'Authorization' not in soap.endpoint_mock.handlers[0].call['requests'][1].headers
        assert b'wsse:UsernameToken' in soap.endpoint_mock.handlers[0].call['requests'][1].body
