# -*- coding: utf-8 -*-
#
# ** header v3.0
# This file is a part of the LinkAhead Project.
#
# Copyright (C) 2018 Research Group Biomedical Physics,
# Max-Planck-Institute for Dynamics and Self-Organization Göttingen
#
# 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 <https://www.gnu.org/licenses/>.
#
# ** end header
#
"""Test linkahead.connection."""
# pylint: disable=missing-docstring
from __future__ import print_function, unicode_literals

import io
import re
from builtins import bytes, str  # pylint: disable=redefined-builtin

import requests

from linkahead import execute_query
from linkahead.configuration import _reset_config, get_config
from linkahead.connection.authentication.interface import CredentialsAuthenticator
from linkahead.connection.connection import (CaosDBServerConnection,
                                             _DefaultCaosDBServerConnection,
                                             _WrappedHTTPResponse,
                                             configure_connection)
from linkahead.connection.mockup import (MockUpResponse, MockUpServerConnection,
                                         _request_log_message)
from linkahead.connection.utils import make_uri_path, quote, urlencode
from linkahead.exceptions import (ConfigurationError, LoginFailedError,
                                  LinkAheadConnectionError)
from pytest import raises


def eq(a, b):
    assert a == b


def there(a):
    assert a is not None


def tru(a):
    assert a


def setup_function(function):
    configure_connection(url="http://localhost:8888/some/path",
                         password_method="plain", username="test",
                         password="blub",
                         implementation=MockUpServerConnection)


def setup_module():
    _reset_config()


def test_quote():
    unenc = 'ö'
    eq('%C3%B6', quote(unenc))

    unenc = '\xf6'
    eq('%C3%B6', quote(unenc))

    unenc = str('ö')
    eq('%C3%B6', quote(unenc))


def test_urlencode():
    eq(urlencode({}), '')
    eq(urlencode({'key1': 'val1'}), 'key1=val1')
    eq(urlencode({'keynoval': None}), 'keynoval=')
    eq(urlencode({'kèy': 'välüe'}), 'k%C3%A8y=v%C3%A4l%C3%BCe')
    with raises(AttributeError) as exc_info:
        urlencode({bytes('asdf', 'utf-8'): 'asdf'})
    with raises(AttributeError) as exc_info:
        urlencode({'asdf': bytes('asdf', 'utf-8')})
    with raises(AttributeError) as exc_info:
        urlencode({None: 'asdf'})


def test_make_uri_path():
    eq(make_uri_path(None, None), '')
    eq(make_uri_path(None, {"key": "val"}), "?key=val")
    eq(make_uri_path([]), "")
    eq(make_uri_path(['a', 'b']), 'a/b')
    eq(make_uri_path(['a', str('b')]), 'a/b')
    eq(make_uri_path(['a', 'ö']), 'a/%C3%B6')
    eq(make_uri_path(['a', str('ö')]), 'a/%C3%B6')
    eq(make_uri_path(['a', 'b'], {'key': 'val'}), 'a/b?key=val')
    eq(make_uri_path([], {"key": "val"}), "?key=val")
    eq(make_uri_path([], {"key": str("val")}), "?key=val")
    eq(make_uri_path([], {"key": None}), "?key=")
    tru(
        re.match(r"^\?(key1=val1&key2=val2|key2=val2&key1=val1)$",
                 make_uri_path([], {
                     "key1": "val1",
                     "key2": "val2"
                 })))
    eq(make_uri_path(['a', 'with/slash']), 'a/with%2Fslash')


def test_configure_connection():
    if not get_config().has_section("Connection"):
        get_config().add_section("Connection")
    get_config().set("Connection", "url", "https://host.de")
    get_config().set("Connection", "username", "test_username")
    get_config().set("Connection", "password_method", "plain")
    get_config().set("Connection", "password", "test_password")
    get_config().set("Connection", "timeout", "200")
    get_config().set("Connection", "ssl_insecure", "True")

    there(configure_connection)
    tru(hasattr(configure_connection, "__call__"))
    c = configure_connection()
    there(c)
    tru(isinstance(c._delegate_connection, _DefaultCaosDBServerConnection))
    tru(isinstance(c._authenticator, CredentialsAuthenticator))

    c = configure_connection(
        implementation=MockUpServerConnection)
    tru(isinstance(c._delegate_connection, MockUpServerConnection))


def test_configure_connection_bad_url():
    configure_connection(url="https://localhost:8888")
    with raises(LinkAheadConnectionError) as exc_info:
        configure_connection(url="ftp://localhost:8888")
    assert exc_info.value.args[0].startswith(
        "The connection url is expected to be a http or https url")
    with raises(LinkAheadConnectionError) as exc_info:
        configure_connection(url="localhost:8888")
    assert exc_info.value.args[0].startswith(
        "The connection url is expected to be a http or https url")


def test_connection_interface():
    with raises(TypeError) as cm:
        CaosDBServerConnection()

    assert "Can't instantiate abstract class CaosDBServerConnection" in str(cm.value)

    tru(hasattr(CaosDBServerConnection, "request"))
    tru(hasattr(CaosDBServerConnection.request, "__call__"))

    tru(hasattr(CaosDBServerConnection, "configure"))
    tru(hasattr(CaosDBServerConnection.configure, "__call__"))


def test_use_mockup_implementation():
    with raises(RuntimeError) as rerr:
        execute_query("FIND Something")
    print(str(rerr.value))
    eq(str(rerr.value),
       "No response for this request - GET: Entity?query=FIND%20Something")


def test_request_log_message():
    headers = {"k0": "v0"}
    body = "this is the body"
    eq(
        _request_log_message(
            method="GET", path="path", headers=None, body=None), "GET: path")
    eq(
        _request_log_message(
            method="GET", path="path", headers=headers, body=None),
        "GET: path\n" + str(headers))
    eq(
        _request_log_message(
            method="GET", path="path", headers=headers, body=body),
        "GET: path\n" + str(headers) + "\n" + str(body))


def test_init_response():
    response = MockUpResponse(
        status=200, headers={"sessionToken": "SessionToken"}, body="Body")
    there(response)

    return response


def test_getter_status():
    response = test_init_response()
    eq(response.status, 200)


def test_read():
    response = test_init_response()
    tru(hasattr(response, "read"))
    eq(response.read().decode(), "Body")


def test_getter_session_token():
    response = test_init_response()
    tru(hasattr(response, "getheader"))
    eq(response.getheader("sessionToken"), "SessionToken")


def test_init_connection():
    connection = MockUpServerConnection()
    there(connection)

    return connection


def test_resources_list():
    connection = test_init_connection()
    assert hasattr(connection, "resources")
    assert len(connection.resources) == 2
    connection.resources.append(lambda **kwargs: test_init_response())
    assert len(connection.resources) == 3

    return connection


def test_request_basics():
    connection = test_init_connection()
    tru(hasattr(connection, "request"))
    with raises(RuntimeError) as cm:
        connection.request(method="GET", path="asdf")
    eq(str(cm.value), "No response for this request - GET: asdf")
    connection = test_resources_list()
    there(connection.request(method="GET", path="asdf"))


def setup_two_resources():
    def r1(**kwargs):
        if kwargs["method"] == "GET":
            return MockUpResponse(status=200, headers=kwargs["headers"], body="response r1")

    def r2(**kwargs):
        if kwargs["path"] == "matching/path/":
            return MockUpResponse(
                status=456, headers={"key": "val"}, body="response r2")

    def r3(**kwargs):
        if kwargs["path"] == "401":
            return MockUpResponse(
                status=401, headers={}, body="please login")

    connection = test_init_connection()
    connection.resources.extend([r1, r2, r3])

    return connection


def test_test_request_with_two_responses():
    connection = setup_two_resources()
    eq(connection.request(method="GET", path="any", headers={}).status, 200)
    eq(connection.request(method="POST", path="matching/path/").status, 456)


def test_missing_implementation():
    connection = configure_connection()
    with raises(ConfigurationError) as exc_info:
        connection.configure()
    assert exc_info.value.args[0].startswith(
        "Missing CaosDBServerConnection implementation.")


def test_bad_implementation_not_callable():
    connection = configure_connection()
    with raises(ConfigurationError) as exc_info:
        connection.configure(implementation=None)
    assert exc_info.value.args[0].startswith(
        "Bad CaosDBServerConnection implementation.")
    assert "'NoneType' object is not callable" in exc_info.value.args[0]


def test_bad_implementation_wrong_class():
    connection = configure_connection()
    with raises(ConfigurationError) as exc_info:
        connection.configure(implementation=dict)
    assert exc_info.value.args[0].startswith(
        "Bad CaosDBServerConnection implementation.")
    assert ("The `implementation` callable did not return an instance of "
            "CaosDBServerConnection.") in exc_info.value.args[0]


def test_missing_auth_method():
    connection = configure_connection()
    with raises(ConfigurationError) as exc_info:
        connection.configure(implementation=MockUpServerConnection)
    assert exc_info.value.args[0].startswith("Missing password_method.")


def test_missing_password():
    connection = configure_connection()
    connection.configure(implementation=setup_two_resources,
                         password_method="plain")
    connection._authenticator.auth_token = "[test-auth-token]"
    assert connection.retrieve(
        ["some"]).headers["Cookie"] == "SessionToken=%5Btest-auth-token%5D;"

    connection.configure(implementation=setup_two_resources,
                         password_method="plain")
    with raises(LoginFailedError):
        connection.delete(["401"])


def test_auth_token_connection():
    connection = configure_connection(auth_token="blablabla",
                                      password_method="auth_token",
                                      implementation=setup_two_resources)
    connection.retrieve(
        ["some"]).headers["Cookie"] == "SessionToken=blablabla;"

    connection._logout()
    with raises(LoginFailedError) as cm:
        connection.retrieve(
            ["some"]).headers["Cookie"] == "SessionToken=blablabla;"
    assert cm.value.args[0] == ("The authentication token is expired or you "
                                "have been logged out otherwise. The "
                                "auth_token authenticator cannot log in "
                                "again. You must provide a new authentication "
                                "token.")


def test_buffer_read():
    """Test the buffering in _WrappedHTTPResponse.read()"""

    class MockResponse(requests.Response):
        def __init__(self, content: bytes):
            """A mock response

            Parameters
            ----------
            content : bytes
              The fake content.
            """
            super().__init__()
            self._content = content
            bio = io.BytesIO(expected)
            self.raw = bio

    expected = b"This response."
    MockResponse(expected)

    #############################
    # Check for some exceptions #
    #############################
    resp = _WrappedHTTPResponse(response=MockResponse(expected))
    with raises(BufferError) as rte:
        resp.read(4)
        resp.read()
    assert "`size` parameter can not be None" in str(rte.value)

    resp = _WrappedHTTPResponse(response=MockResponse(expected))
    with raises(BufferError) as rte:
        resp.read(4)
        resp.read(0)
    assert "`size` parameter can not be None" in str(rte.value)

    print("---")
    resp = _WrappedHTTPResponse(response=MockResponse(expected))
    result = (
        resp.read(4)
        + resp.read(2)
        + resp.read(2)  # This line failed before.
        + resp.read(4)  # Reading the rest in two chunks, because of current limitations in read().
        + resp.read(2)
    )

    assert result == expected