#
#    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.

import collections
from unittest import mock
import uuid

import lxml.etree
import lxml.objectify
import requests.exceptions
import requests_mock
import six

from dracclient import exceptions
from dracclient.tests import base
from dracclient.tests import utils as test_utils
import dracclient.wsman


class ClientTestCase(base.BaseTest):

    def setUp(self):
        super(ClientTestCase, self).setUp()
        self.client = dracclient.wsman.Client(**test_utils.FAKE_ENDPOINT)

    @requests_mock.Mocker()
    def test_enumerate(self, mock_requests):
        expected_resp = '<result>yay!</result>'
        mock_requests.post('https://1.2.3.4:443/wsman', text=expected_resp)

        resp = self.client.enumerate('resource', auto_pull=False)
        self.assertEqual('yay!', resp.text)

    def test_enumerate_with_request_failure(self):
        self.client = dracclient.wsman.Client('malformed://^@*', 'user',
                                              'pass')

        self.assertRaises(exceptions.WSManRequestFailure,
                          self.client.enumerate, 'resource')

    @requests_mock.Mocker()
    def test_enumerate_with_invalid_status_code(self, mock_requests):
        mock_requests.post('https://1.2.3.4:443/wsman', status_code=500,
                           reason='dumb request')

        self.assertRaises(exceptions.WSManInvalidResponse,
                          self.client.enumerate, 'resource')

    @requests_mock.Mocker()
    def test_enumerate_with_invalid_utf8(self, mock_requests):
        mock_requests.post('https://1.2.3.4:443/wsman',
                           content=six.b('<result>yay!\xC0</result>'))

        resp = self.client.enumerate('resource')
        self.assertEqual('yay!', resp.text)

    @requests_mock.Mocker()
    def test_enumerate_with_auto_pull(self, mock_requests):
        mock_requests.post(
            'https://1.2.3.4:443/wsman',
            [{'text': test_utils.WSManEnumerations['context'][0]},
             {'text': test_utils.WSManEnumerations['context'][1]},
             {'text': test_utils.WSManEnumerations['context'][2]},
             {'text': test_utils.WSManEnumerations['context'][3]}])

        resp_xml = self.client.enumerate('FooResource')

        foo_resource_uri = 'http://FooResource'
        bar_resource_uri = 'http://BarResource'
        self.assertEqual(
            4, len(resp_xml.findall('.//{%s}FooResource' % foo_resource_uri)))
        self.assertEqual(
            1, len(resp_xml.findall('.//{%s}BazResource' % bar_resource_uri)))
        self.assertEqual(
            0, len(resp_xml.findall(
                './/{%s}EnumerationContext' % dracclient.wsman.NS_WSMAN_ENUM)))

    @requests_mock.Mocker()
    @mock.patch.object(dracclient.wsman.Client, 'pull', autospec=True)
    def test_enumerate_with_auto_pull_without_optimization(self, mock_requests,
                                                           mock_pull):
        mock_requests.post('https://1.2.3.4:443/wsman',
                           text=test_utils.WSManEnumerations['context'][0])
        mock_pull.return_value = lxml.etree.fromstring(
            test_utils.WSManEnumerations['context'][3])

        self.client.enumerate('FooResource', optimization=False, max_elems=42)

        mock_pull.assert_called_once_with(self.client, 'FooResource',
                                          'enum-context-uuid', 42)

    @requests_mock.Mocker()
    def test_pull(self, mock_requests):
        expected_resp = '<result>yay!</result>'
        mock_requests.post('https://1.2.3.4:443/wsman', text=expected_resp)

        resp = self.client.pull('resource', 'context-uuid')

        self.assertEqual('yay!', resp.text)

    @requests_mock.Mocker()
    def test_invoke(self, mock_requests):
        expected_resp = '<result>yay!</result>'
        mock_requests.post('https://1.2.3.4:443/wsman', text=expected_resp)

        resp = self.client.invoke('http://resource', 'method',
                                  {'selector': 'foo'}, {'property': 'bar'})

        self.assertEqual('yay!', resp.text)

    @requests_mock.Mocker()
    def test_invoke_with_ssl_errors(self, mock_requests):
        mock_requests.post('https://1.2.3.4:443/wsman',
                           exc=requests.exceptions.SSLError)

        self.assertRaises(exceptions.WSManRequestFailure,
                          self.client.invoke, 'http://resource', 'method',
                          {'selector': 'foo'}, {'property': 'bar'})

    @requests_mock.Mocker()
    def test_invoke_with_ssl_error_success(self, mock_requests):
        expected_resp = '<result>yay!</result>'
        mock_requests.post('https://1.2.3.4:443/wsman',
                           [{'exc': requests.exceptions.SSLError},
                            {'text': expected_resp}])

        resp = self.client.invoke('http://resource', 'method',
                                  {'selector': 'foo'}, {'property': 'bar'})

        self.assertEqual('yay!', resp.text)

    @requests_mock.Mocker()
    def test_invoke_with_connection_errors(self, mock_requests):
        mock_requests.post('https://1.2.3.4:443/wsman',
                           exc=requests.exceptions.ConnectionError)

        self.assertRaises(exceptions.WSManRequestFailure,
                          self.client.invoke, 'http://resource', 'method',
                          {'selector': 'foo'}, {'property': 'bar'})

    @requests_mock.Mocker()
    def test_invoke_with_connection_error_success(self, mock_requests):
        expected_resp = '<result>yay!</result>'
        mock_requests.post('https://1.2.3.4:443/wsman',
                           [{'exc': requests.exceptions.ConnectionError},
                            {'text': expected_resp}])

        resp = self.client.invoke('http://resource', 'method',
                                  {'selector': 'foo'}, {'property': 'bar'})

        self.assertEqual('yay!', resp.text)

    @requests_mock.Mocker()
    def test_invoke_with_unknown_error(self, mock_requests):
        mock_requests.post('https://1.2.3.4:443/wsman',
                           exc=requests.exceptions.HTTPError)
        self.assertRaises(exceptions.WSManRequestFailure,
                          self.client.invoke, 'http://resource', 'method',
                          {'selector': 'foo'}, {'property': 'bar'})

    @requests_mock.Mocker()
    @mock.patch('time.sleep', autospec=True)
    def test_client_retry_delay(self, mock_requests, mock_ts):
        ssl_retry_delay = 5
        fake_endpoint = test_utils.FAKE_ENDPOINT.copy()
        fake_endpoint['ssl_retry_delay'] = ssl_retry_delay
        client = dracclient.wsman.Client(**fake_endpoint)
        expected_resp = '<result>yay!</result>'
        mock_requests.post('https://1.2.3.4:443/wsman',
                           [{'exc': requests.exceptions.SSLError},
                            {'text': expected_resp}])

        resp = client.invoke('http://resource', 'method',
                             {'selector': 'foo'}, {'property': 'bar'})

        self.assertEqual('yay!', resp.text)
        mock_ts.assert_called_once_with(ssl_retry_delay)


class PayloadTestCase(base.BaseTest):

    def setUp(self):
        super(PayloadTestCase, self).setUp()
        dracclient.wsman.NS_MAP = collections.OrderedDict([
            ('s', dracclient.wsman.NS_SOAP_ENV),
            ('wsa', dracclient.wsman.NS_WS_ADDR),
            ('wsman', dracclient.wsman.NS_WSMAN)])

    @mock.patch.object(uuid, 'uuid4', autospec=True)
    def test_build_enum(self, mock_uuid):
        expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
            xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
    <s:Header>
        <wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
        <wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
        <wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
        <wsa:ReplyTo>
            <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
        </wsa:ReplyTo>
        <wsa:Action s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate</wsa:Action>
    </s:Header>
    <s:Body>
        <wsen:Enumerate xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
            <wsman:OptimizeEnumeration/>
            <wsman:MaxElements>100</wsman:MaxElements>
        </wsen:Enumerate>
    </s:Body>
</s:Envelope>
"""  # noqa
        expected_payload_obj = lxml.objectify.fromstring(expected_payload)

        mock_uuid.return_value = '1234-12'
        payload = dracclient.wsman._EnumeratePayload(
            'http://host:443/wsman', 'http://resource_uri').build()
        payload_obj = lxml.objectify.fromstring(payload)

        self.assertEqual(lxml.etree.tostring(expected_payload_obj),
                         lxml.etree.tostring(payload_obj))

    def test_enumerate_without_optimization(self):
        payload = dracclient.wsman._EnumeratePayload(
            'http://host:443/wsman', 'http://resource_uri', optimization=False,
            max_elems=42).build()
        payload_xml = lxml.etree.fromstring(payload)

        optimize_enum_elems = payload_xml.findall(
            './/{%s}OptimizeEnumeration' % dracclient.wsman.NS_WSMAN)
        max_elem_elems = payload_xml.findall(
            './/{%s}MaxElements' % dracclient.wsman.NS_WSMAN)
        self.assertEqual([], optimize_enum_elems)
        self.assertEqual([], max_elem_elems)

    @mock.patch.object(uuid, 'uuid4', autospec=True)
    def test_build_enum_with_filter(self, mock_uuid):
        expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
            xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
    <s:Header>
        <wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
        <wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
        <wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
        <wsa:ReplyTo>
            <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
        </wsa:ReplyTo>
        <wsa:Action s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate</wsa:Action>
    </s:Header>
    <s:Body>
        <wsen:Enumerate xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
            <wsman:Filter Dialect="http://schemas.dmtf.org/wbem/cql/1/dsp0202.pdf">DROP TABLE users</wsman:Filter>
            <wsman:OptimizeEnumeration/>
            <wsman:MaxElements>100</wsman:MaxElements>
        </wsen:Enumerate>
    </s:Body>
</s:Envelope>
"""  # noqa
        expected_payload_obj = lxml.objectify.fromstring(expected_payload)

        mock_uuid.return_value = '1234-12'
        payload = dracclient.wsman._EnumeratePayload(
            'http://host:443/wsman', 'http://resource_uri',
            filter_query='DROP TABLE users', filter_dialect='cql').build()
        payload_obj = lxml.objectify.fromstring(payload)

        self.assertEqual(lxml.etree.tostring(expected_payload_obj),
                         lxml.etree.tostring(payload_obj))

    def test_build_enum_with_invalid_filter_dialect(self):
        invalid_dialect = 'foo'
        self.assertRaises(exceptions.WSManInvalidFilterDialect,
                          dracclient.wsman._EnumeratePayload,
                          'http://host:443/wsman', 'http://resource_uri',
                          filter_query='DROP TABLE users',
                          filter_dialect=invalid_dialect)

    @mock.patch.object(uuid, 'uuid4', autospec=True)
    def test_build_pull(self, mock_uuid):
        expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
            xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
    <s:Header>
        <wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
        <wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
        <wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
        <wsa:ReplyTo>
            <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
        </wsa:ReplyTo>
        <wsa:Action s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull</wsa:Action>
    </s:Header>
    <s:Body>
        <wsen:Pull xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
            <wsen:EnumerationContext>context-uuid</wsen:EnumerationContext>
            <wsman:MaxElements>100</wsman:MaxElements>
        </wsen:Pull>
    </s:Body>
</s:Envelope>
"""  # noqa
        expected_payload_obj = lxml.objectify.fromstring(expected_payload)

        mock_uuid.return_value = '1234-12'
        payload = dracclient.wsman._PullPayload('http://host:443/wsman',
                                                'http://resource_uri',
                                                'context-uuid').build()
        payload_obj = lxml.objectify.fromstring(payload)

        self.assertEqual(lxml.etree.tostring(expected_payload_obj),
                         lxml.etree.tostring(payload_obj))

    @mock.patch.object(uuid, 'uuid4', autospec=True)
    def test_build_invoke(self, mock_uuid):
        expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
            xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
    <s:Header>
        <wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
        <wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
        <wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
        <wsa:ReplyTo>
            <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
        </wsa:ReplyTo>
        <wsa:Action s:mustUnderstand="true">http://resource_uri/method</wsa:Action>
        <wsman:SelectorSet>
            <wsman:Selector Name="selector">foo</wsman:Selector>
        </wsman:SelectorSet>
    </s:Header>
    <s:Body>
        <ns0:method_INPUT xmlns:ns0="http://resource_uri">
            <ns0:property>bar</ns0:property>
        </ns0:method_INPUT>
    </s:Body>
</s:Envelope>
"""  # noqa
        expected_payload_obj = lxml.objectify.fromstring(expected_payload)

        mock_uuid.return_value = '1234-12'
        payload = dracclient.wsman._InvokePayload(
            'http://host:443/wsman', 'http://resource_uri', 'method',
            {'selector': 'foo'}, {'property': 'bar'}).build()
        payload_obj = lxml.objectify.fromstring(payload)

        self.assertEqual(lxml.etree.tostring(expected_payload_obj),
                         lxml.etree.tostring(payload_obj))

    @mock.patch.object(uuid, 'uuid4', autospec=True)
    def test_build_invoke_with_list_in_properties(self, mock_uuid):
        expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
            xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
    <s:Header>
        <wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
        <wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
        <wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
        <wsa:ReplyTo>
            <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
        </wsa:ReplyTo>
        <wsa:Action s:mustUnderstand="true">http://resource_uri/method</wsa:Action>
        <wsman:SelectorSet>
            <wsman:Selector Name="selector">foo</wsman:Selector>
        </wsman:SelectorSet>
    </s:Header>
    <s:Body>
        <ns0:method_INPUT xmlns:ns0="http://resource_uri">
            <ns0:property>foo</ns0:property>
            <ns0:property>bar</ns0:property>
            <ns0:property>baz</ns0:property>
        </ns0:method_INPUT>
    </s:Body>
</s:Envelope>
"""  # noqa
        expected_payload_obj = lxml.objectify.fromstring(expected_payload)

        mock_uuid.return_value = '1234-12'
        payload = dracclient.wsman._InvokePayload(
            'http://host:443/wsman', 'http://resource_uri', 'method',
            {'selector': 'foo'}, {'property': ['foo', 'bar', 'baz']}).build()
        payload_obj = lxml.objectify.fromstring(payload)

        self.assertEqual(lxml.etree.tostring(expected_payload_obj),
                         lxml.etree.tostring(payload_obj))
