from unittest import mock
from urllib.parse import parse_qs, urlparse

from django.http import QueryDict
from django.test import Client
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property

from .utils import create_root, create_team, create_user


class TestCaseMixin:
    """Extends TestCase for kfet application tests."""

    def assertForbidden(self, response):
        """
        Test that the response (retrieved with a Client) is a denial of access.

        The response should verify one of the following:
        - its HTTP response code is 403,
        - it redirects to the login page with a GET parameter named 'next'
        whose value is the url of the requested page.

        """
        request = response.wsgi_request

        try:
            try:
                # Is this an HTTP Forbidden response ?
                self.assertEqual(response.status_code, 403)
            except AssertionError:
                # A redirection to the login view is fine too.

                # Let's build the login url with the 'next' param on current
                # page.
                full_path = request.get_full_path()

                querystring = QueryDict(mutable=True)
                querystring["next"] = full_path

                login_url = "/gestion/login?" + querystring.urlencode(safe="/")

                # We don't focus on what the login view does.
                # So don't fetch the redirect.
                self.assertRedirects(response, login_url, fetch_redirect_response=False)
        except AssertionError:
            raise AssertionError(
                "%(http_method)s request at %(path)s should be forbidden for "
                "%(username)s user.\n"
                "Response isn't 403, nor a redirect to login view. Instead, "
                "response code is %(code)d."
                % {
                    "http_method": request.method,
                    "path": request.get_full_path(),
                    "username": (
                        "'{}'".format(request.user)
                        if request.user.is_authenticated
                        else "anonymous"
                    ),
                    "code": response.status_code,
                }
            )

    def assertForbiddenKfet(self, response, form_ctx="form"):
        """
        Test that a response (retrieved with a Client) contains error due to
        lack of kfet permissions.

        It checks that 'Permission refusée' is present in the non-field errors
        of the form of response context at key 'form_ctx', or present in
        messages.

        This should be used for pages which can be accessed by the kfet team
        members, but require additionnal permission(s) to make an operation.

        """
        try:
            self.assertEqual(response.status_code, 200)
            try:
                form = response.context[form_ctx]
                errors = [y for x in form.errors.as_data().values() for y in x]
                self.assertTrue(any(e.code == "permission-denied" for e in errors))
            except (AssertionError, AttributeError, KeyError):
                self.assertTrue(
                    any(
                        "permission-denied" in msg.tags
                        for msg in response.context["messages"]
                    )
                )
        except AssertionError:
            request = response.wsgi_request
            raise AssertionError(
                "%(http_method)s request at %(path)s should raise an error "
                "for %(username)s user.\n"
                "Cannot find any errors in non-field errors of form "
                "'%(form_ctx)s', nor in messages."
                % {
                    "http_method": request.method,
                    "path": request.get_full_path(),
                    "username": (
                        "'%s'" % request.user
                        if request.user.is_authenticated
                        else "anonymous"
                    ),
                    "form_ctx": form_ctx,
                }
            )

    def assertInstanceExpected(self, instance, expected):
        """
        Test that the values of the attributes and without-argument methods of
        'instance' are equal to 'expected' pairs.
        """
        for attr, expected_value in expected.items():
            value = getattr(instance, attr)
            if callable(value):
                value = value()
            self.assertEqual(value, expected_value)

    def assertUrlsEqual(self, actual, expected):
        """
        Test that the url 'actual' is as 'expected'.

        Arguments:
            actual (str): Url to verify.
            expected: Two forms are accepted.
                * (str): Expected url. Strings equality is checked.
                * (dict): Its keys must be attributes of 'urlparse(actual)'.
                    Equality is checked for each present key, except for
                    'query' which must be a dict of the expected query string
                    parameters.

        """
        if type(expected) == dict:
            parsed = urlparse(actual)
            for part, expected_part in expected.items():
                if part == "query":
                    self.assertDictEqual(
                        parse_qs(parsed.query), expected.get("query", {})
                    )
                else:
                    self.assertEqual(getattr(parsed, part), expected_part)
        else:
            self.assertEqual(actual, expected)


class ViewTestCaseMixin(TestCaseMixin):
    """
    TestCase extension to ease tests of kfet views.


    Urls concerns
    -------------

    # Basic usage

    Attributes:
        url_name (str): Name of view under test, as given to 'reverse'
            function.
        url_args (list, optional): Will be given to 'reverse' call.
        url_kwargs (dict, optional): Same.
        url_expcted (str): What 'reverse' should return given previous
            attributes.

    View url can then be accessed at the 'url' attribute.

    # Advanced usage

    If multiple combinations of url name, args, kwargs can be used for a view,
    it is possible to define 'urls_conf' attribute. It must be a list whose
    each item is a dict defining arguments for 'reverse' call ('name', 'args',
    'kwargs' keys) and its expected result ('expected' key).

    The reversed urls can be accessed at the 't_urls' attribute.


    Users concerns
    --------------

    During setup, three users are created with their kfet account:
    - 'user': a basic user without any permission, account trigramme: 000,
    - 'team': a user with kfet.is_team permission, account trigramme: 100,
    - 'root': a superuser, account trigramme: 200,
    - 'liq': if class attribute 'with_liq' is 'True', account
      trigramme: LIQ.
    Their password is their username.

    One can create additionnal users with 'get_users_extra' method, or prevent
    these 3 users to be created with 'get_users_base' method. See these two
    methods for further informations.

    By using 'register_user' method, these users can then be accessed at
    'users' attribute by their label. Similarly, their kfet account is
    registered on 'accounts' attribute.

    A user label can be given to 'auth_user' attribute. The related user is
    then authenticated on self.client during test setup. Its value defaults to
    'None', meaning no user is authenticated.


    Automated tests
    ---------------

    # Url reverse

    Based on url-related attributes/properties, the test 'test_urls' checks
    that expected url is returned by 'reverse' (once with basic url usage and
    each for advanced usage).

    # Forbidden responses

    The 'test_forbidden' test verifies that each user, from labels of
    'auth_forbidden' attribute, can't access the url(s), i.e. response should
    be a 403, or a redirect to login view.

    Tested HTTP requests are given by 'http_methods' attribute. Additional data
    can be given by defining an attribute '<method(lowercase)>_data'.

    """

    url_name = None
    url_expected = None

    http_methods = ["GET"]

    auth_user = None
    auth_forbidden = []

    with_liq = False

    def setUp(self):
        """
        Warning: Do not forget to call super().setUp() in subclasses.
        """
        # Signals handlers on login/logout send messages.
        # Due to the way the Django' test Client performs login, this raise an
        # error. As workaround, we mock the Django' messages module.
        patcher_messages = mock.patch("gestioncof.signals.messages")
        patcher_messages.start()
        self.addCleanup(patcher_messages.stop)

        # A test can mock 'django.utils.timezone.now' and give this as return
        # value. E.g. it is useful if the test checks values of 'auto_now' or
        # 'auto_now_add' fields.
        self.now = timezone.now()

        # These attributes register users and accounts instances.
        self.users = {}
        self.accounts = {}

        for label, user in dict(self.users_base, **self.users_extra).items():
            self.register_user(label, user)

        if self.auth_user:
            self.client.force_login(
                self.users[self.auth_user],
                backend="django.contrib.auth.backends.ModelBackend",
            )

    def tearDown(self):
        del self.users_base
        del self.users_extra

    def get_users_base(self):
        """
        Dict of <label: user instance>.

        Note: Don't access yourself this property. Use 'users_base' attribute
        which cache the returned value from here.
        It allows to give functions calls, which creates users instances, as
        values here.

        """
        # Format desc: username, password, trigramme
        users_base = {
            # user, user, 000
            "user": create_user(),
            # team, team, 100
            "team": create_team(),
            # root, root, 200
            "root": create_root(),
        }
        if self.with_liq:
            users_base["liq"] = create_user("liq", "LIQ")
        return users_base

    @cached_property
    def users_base(self):
        return self.get_users_base()

    def get_users_extra(self):
        """
        Dict of <label: user instance>.

        Note: Don't access yourself this property. Use 'users_base' attribute
        which cache the returned value from here.
        It allows to give functions calls, which create users instances, as
        values here.

        """
        return {}

    @cached_property
    def users_extra(self):
        return self.get_users_extra()

    def register_user(self, label, user):
        self.users[label] = user
        if hasattr(user.profile, "account_kfet"):
            self.accounts[label] = user.profile.account_kfet

    def get_user(self, label):
        if self.auth_user is not None:
            return self.auth_user
        return self.auth_user_mapping.get(label)

    @property
    def urls_conf(self):
        return [
            {
                "name": self.url_name,
                "args": getattr(self, "url_args", []),
                "kwargs": getattr(self, "url_kwargs", {}),
                "expected": self.url_expected,
            }
        ]

    @property
    def t_urls(self):
        return [
            reverse(
                url_conf["name"],
                args=url_conf.get("args", []),
                kwargs=url_conf.get("kwargs", {}),
            )
            for url_conf in self.urls_conf
        ]

    @property
    def url(self):
        return self.t_urls[0]

    def test_urls(self):
        for url, conf in zip(self.t_urls, self.urls_conf):
            self.assertEqual(url, conf["expected"])

    def test_forbidden(self):
        for method in self.http_methods:
            for user in self.auth_forbidden:
                for url in self.t_urls:
                    self.check_forbidden(method, url, user)

    def check_forbidden(self, method, url, user=None):
        method = method.lower()
        client = Client()
        if user is not None:
            client.login(username=user, password=user)

        send_request = getattr(client, method)
        data = getattr(self, "{}_data".format(method), {})

        r = send_request(url, data)
        self.assertForbidden(r)
