# -*- coding: utf-8 -*-
# (c) 2018 Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import annotations

import io
import sys
import http.client
import urllib.error
from http.cookiejar import Cookie

from ansible.module_utils.urls import fetch_url, is_fetch_success, ConnectionError

import pytest
from unittest.mock import MagicMock

BASE_URL = 'https://ansible.com/'


class AnsibleModuleExit(Exception):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs


class ExitJson(AnsibleModuleExit):
    pass


class FailJson(AnsibleModuleExit):
    pass


@pytest.fixture
def open_url_mock(mocker):
    return mocker.patch('ansible.module_utils.urls.open_url')


@pytest.fixture
def fake_ansible_module():
    return FakeAnsibleModule()


class FakeAnsibleModule:
    def __init__(self):
        self.params = {}
        self.tmpdir = None

    def exit_json(self, *args, **kwargs):
        raise ExitJson(*args, **kwargs)

    def fail_json(self, *args, **kwargs):
        raise FailJson(*args, **kwargs)


def test_fetch_url(open_url_mock, fake_ansible_module):
    r, info = fetch_url(fake_ansible_module, BASE_URL)

    dummy, kwargs = open_url_mock.call_args

    open_url_mock.assert_called_once_with(BASE_URL, client_cert=None, client_key=None, cookies=kwargs['cookies'], data=None,
                                          follow_redirects='urllib2', force=False, force_basic_auth='', headers=None,
                                          http_agent='ansible-httpget', last_mod_time=None, method=None, timeout=10, url_password='', url_username='',
                                          use_proxy=True, validate_certs=True, use_gssapi=False, unix_socket=None, ca_path=None, unredirected_headers=None,
                                          decompress=True, ciphers=None, use_netrc=True)


def test_fetch_url_params(open_url_mock, fake_ansible_module):
    fake_ansible_module.params = {
        'validate_certs': False,
        'url_username': 'user',
        'url_password': 'passwd',
        'http_agent': 'ansible-test',
        'force_basic_auth': True,
        'follow_redirects': 'all',
        'client_cert': 'client.pem',
        'client_key': 'client.key',
    }

    r, info = fetch_url(fake_ansible_module, BASE_URL)

    dummy, kwargs = open_url_mock.call_args

    open_url_mock.assert_called_once_with(BASE_URL, client_cert='client.pem', client_key='client.key', cookies=kwargs['cookies'], data=None,
                                          follow_redirects='all', force=False, force_basic_auth=True, headers=None,
                                          http_agent='ansible-test', last_mod_time=None, method=None, timeout=10, url_password='passwd', url_username='user',
                                          use_proxy=True, validate_certs=False, use_gssapi=False, unix_socket=None, ca_path=None, unredirected_headers=None,
                                          decompress=True, ciphers=None, use_netrc=True)


def test_fetch_url_cookies(mocker, fake_ansible_module):
    def make_cookies(*args, **kwargs):
        cookies = kwargs['cookies']
        r = MagicMock()
        r.headers = http.client.HTTPMessage()
        add_header = r.headers.add_header
        r.info.return_value = r.headers
        for name, value in (('Foo', 'bar'), ('Baz', 'qux')):
            cookie = Cookie(
                version=0,
                name=name,
                value=value,
                port=None,
                port_specified=False,
                domain="ansible.com",
                domain_specified=True,
                domain_initial_dot=False,
                path="/",
                path_specified=True,
                secure=False,
                expires=None,
                discard=False,
                comment=None,
                comment_url=None,
                rest=None
            )
            cookies.set_cookie(cookie)
            add_header('Set-Cookie', '%s=%s' % (name, value))

        return r

    mocker = mocker.patch('ansible.module_utils.urls.open_url', new=make_cookies)

    r, info = fetch_url(fake_ansible_module, BASE_URL)

    assert info['cookies'] == {'Baz': 'qux', 'Foo': 'bar'}

    if sys.version_info < (3, 11):
        # Python sorts cookies in order of most specific (ie. longest) path first
        # items with the same path are reversed from response order
        assert info['cookies_string'] == 'Baz=qux; Foo=bar'
    else:
        # Python 3.11 and later preserve the Set-Cookie order.
        # See: https://github.com/python/cpython/pull/22745/
        assert info['cookies_string'] == 'Foo=bar; Baz=qux'

    # The key here has a `-` as opposed to what we see in the `uri` module that converts to `_`
    # Note: this is response order, which differs from cookies_string
    assert info['set-cookie'] == 'Foo=bar, Baz=qux'


def test_fetch_url_connectionerror(open_url_mock, fake_ansible_module):
    open_url_mock.side_effect = ConnectionError('TESTS')
    with pytest.raises(FailJson) as excinfo:
        fetch_url(fake_ansible_module, BASE_URL)

    assert excinfo.value.kwargs['msg'] == 'TESTS'
    assert BASE_URL == excinfo.value.kwargs['url']
    assert excinfo.value.kwargs['status'] == -1

    open_url_mock.side_effect = ValueError('TESTS')
    with pytest.raises(FailJson) as excinfo:
        fetch_url(fake_ansible_module, BASE_URL)

    assert excinfo.value.kwargs['msg'] == 'TESTS'
    assert BASE_URL == excinfo.value.kwargs['url']
    assert excinfo.value.kwargs['status'] == -1


def test_fetch_url_httperror(open_url_mock, fake_ansible_module):
    open_url_mock.side_effect = urllib.error.HTTPError(
        BASE_URL,
        500,
        'Internal Server Error',
        {'Content-Type': 'application/json'},
        io.StringIO('TESTS')
    )

    r, info = fetch_url(fake_ansible_module, BASE_URL)

    assert info == {'msg': 'HTTP Error 500: Internal Server Error', 'body': 'TESTS',
                    'status': 500, 'url': BASE_URL, 'content-type': 'application/json'}


def test_fetch_url_urlerror(open_url_mock, fake_ansible_module):
    open_url_mock.side_effect = urllib.error.URLError('TESTS')
    r, info = fetch_url(fake_ansible_module, BASE_URL)
    assert info == {'msg': 'Request failed: <urlopen error TESTS>', 'status': -1, 'url': BASE_URL}


def test_fetch_url_socketerror(open_url_mock, fake_ansible_module):
    open_url_mock.side_effect = OSError('TESTS')
    r, info = fetch_url(fake_ansible_module, BASE_URL)
    assert info == {'msg': 'Connection failure: TESTS', 'status': -1, 'url': BASE_URL}


def test_fetch_url_exception(open_url_mock, fake_ansible_module):
    open_url_mock.side_effect = Exception('TESTS')
    r, info = fetch_url(fake_ansible_module, BASE_URL)
    exception = info.pop('exception')
    assert info == {'msg': 'An unknown error occurred: TESTS', 'status': -1, 'url': BASE_URL}
    assert "Exception: TESTS" in exception


def test_fetch_url_badstatusline(open_url_mock, fake_ansible_module):
    open_url_mock.side_effect = http.client.BadStatusLine('TESTS')
    r, info = fetch_url(fake_ansible_module, BASE_URL)
    assert info == {'msg': 'Connection failure: connection was closed before a valid response was received: TESTS', 'status': -1, 'url': BASE_URL}


# is_fetch_success is highly coupled to fetch_url, unit test here.
@pytest.mark.parametrize(
    'url, status, additional, expected',
    [
        pytest.param('ftp://some/dir/file.txt', None, None, True, id='ftp_success'),
        pytest.param('ftps://another/dir/f.txt', -1, None, False, id='ftps_failure'),
        pytest.param('ftp://foo/dir/f.md', None, (-1,), True, id='ftp_failure'),
        pytest.param('ftp://foo/dir/f.md', -1, (-1,), True, id='ftp_failure_additional'),
        pytest.param('https://foo.com/file', 200, None, True, id='https_success'),
        pytest.param('https://foo.com/file', 400, None, False, id='https_failure'),
        pytest.param('http://foo.com/file', 200, None, True, id='http_success'),
        pytest.param('http://foo.com/file', 400, None, False, id='http_failure'),
        pytest.param('http://foo.com/file', 220, (220,), True, id='http_success_additional'),
        pytest.param('http://foo.com/file', 200, (220,), True, id='http_success_additional_2'),
        pytest.param('http://foo.com/file', 400, (220,), False, id='http_failure_additional'),
        pytest.param("Some nonsense fetch didn't catch", -1, None, False, id='nonsense_failure'),
    ]
)
def test_is_fetch_success(url, status, additional, expected):
    assert expected == is_fetch_success(dict(url=url, status=status), additional_codes=additional)
