"""Base test cases and tools to use for testing in MTP.

Attributes
----------

DEFAULT_PASSWORD : string
    A default password set to ``MTPUser`` created in tests.
DEFAULT_PASSWORD : string
    A "secure" password that follow some rules. # TODO : add more details

"""

from contextlib import contextmanager
from unittest.mock import patch

import threading

from django.conf import settings
from django.template import Context, Template
from django.test import Client, RequestFactory, TestCase, TransactionTestCase
from django.utils.timezone import utc

from pinax.notifications.models import NoticeType

from mtp.apps.accounts.constants import (
    GROUPTYPE_INTERNAL,
    INTERNAL_GROUP_TRAINEE,
    INTERNAL_GROUP_TRAINER,
    INTERNAL_GROUP_WHOSWHO,
)
from mtp.apps.accounts.factories import (
    AdminFactory,
    GroupFactory,
    GroupRoleFactory,
    GroupTypeFactory,
    JobProfileFactory,
    TraineeFactory,
    TrainerFactory,
)
from mtp.apps.accounts.models import GroupRole
from mtp.apps.themes.factories import ThemeFactory
from mtp.apps.trainings.factories import TrainingFactory, TrainingSessionFactory
from mtp.apps.trainings.models import Training

DEFAULT_PASSWORD = 'magency'
SECURE_PASSWORD = 'Magency2015!'


def run_tests_concurrently(times):
    """Decorator to easily test a method in a concurrent manner, using threads..

    STOLEN from this 6 years old blog post:
    https://www.caktusgroup.com/blog/2009/05/26/testing-django-views-for-concurrency-issues/

    Add this decorator to small pieces of code that you want to test
    concurrently to make sure they don't raise exceptions when run at the
    same time.  E.g., some Django views that do a SELECT and then a subsequent
    INSERT might fail when the INSERT assumes that the data has not changed
    since the SELECT.

    Example
    -------

    >>> from mtp.apps.common.tests.test_cases import run_tests_concurrently
    >>> @run_tests_concurrently(3)
    ... def do_stuff():
    ...     pass  # do_something_that_you_want_to_test_in_a_concurrent_manner()

    Notes
    -----

    It may not work with simple ``TestCase`` if you need database data. Use
    ``TransactionTestCase`` but use it with parsimony because it's a lot slower.

    """

    def test_concurrently_decorator(test_func):
        def wrapper(*args, **kwargs):
            exceptions = []

            def call_test_func():
                try:
                    test_func(*args, **kwargs)
                except Exception as e:
                    exceptions.append(e)
                    raise
            threads = []
            for i in range(times):
                threads.append(threading.Thread(target=call_test_func))
            for t in threads:
                t.start()
            for t in threads:
                t.join()
            if exceptions:
                raise Exception('run_tests_concurrently intercepted %s exceptions: %s' % (
                    len(exceptions), exceptions))
        return wrapper
    return test_concurrently_decorator


@contextmanager
def patch_timezone_now_field(cls, field_name, new_datetime):
    """A context manager to mock value of fields having ``default=timezone.now``.

    Parameters
    ----------
    cls : type
        The model holding the field to update
    field_name : str
        The name of the field to update
    new_datetime : datetime
        The value to use as a default one instead of ``timezone.now``

    Example
    -------

    >>> with patch_timezone_now_field(Answer, 'time', datetime(2015, 6, 1, 0, 0, 0)):
    >>>     do_stuff()

    """
    field = cls._meta.get_field(field_name)
    # Same behaviour as ``django.utils.timewone.now``: add a timezone if
    # ``settings.USE_TZ`` is ``True``.
    if settings.USE_TZ and not new_datetime.tzinfo:
        new_datetime = new_datetime.replace(tzinfo=utc)
    mock_now = lambda: new_datetime
    with patch.object(field, 'default', new=mock_now):
        yield


class BaseMTPTestCase:
    """Base test case used to define ``MTPTestCase`` and ``MTPTransactionTestCase``.

    It provides some default test data useful in a lot of tests.

    Attributes
    ----------
    admin : MTPUser
        A ``MTPUser` with admin rights. Use the ``DEFAULT_PASSWORD`` constant as password.
    trainer : MTPUser
        A ``MTPUser` with trainer rights. Use the ``DEFAULT_PASSWORD`` constant as password.
    trainees : list of MTPUser
        A list of ``MTPUser`` with trainee rights. Use the ``DEFAULT_PASSWORD`` constant as
        password.
    trainee : MTPUser
        First entry of the ``trainees`` attribute, for easy access.
    training : Training
        A simple ``Training`` instance.
    training_session : TrainingSession
        A ``TrainingSession`` instance for the ``training`` and the ``trainer``.
    client : django.test.Client
        A django test client, that will follow redirects.

    """

    def login_as_admin(self, username=None, password=DEFAULT_PASSWORD):
        if username is None:
            username = self.admin.username
        self.client.login(username=username, password=password)

    def login_as_trainer(self, username=None, password=DEFAULT_PASSWORD):
        if username is None:
            username = self.trainer.username
        self.client.login(username=username, password=password)

    def login_as_trainee(self, username=None, password=DEFAULT_PASSWORD):
        if username is None:
            username = self.trainees[0].username
        self.client.login(username=username, password=password)

    @classmethod
    def setUpTestData(cls):
        """Define test data on a class basis.

        Data will be recreated for each test, using db transactions, instead of recreating them
        on each call to ``setUp``.

        """

        if hasattr(super(), 'setupTestData'):
            # ``TransactionTestCase`` has no ``setUpTestData``, and this base class is
            # used for ``MTPTransactionTestCase``, so we'll check before calling it if it
            # exists.
            super().setUpTestData()

        # Create default groups
        internal_group_type = GroupTypeFactory(name=GROUPTYPE_INTERNAL)
        cls.trainer_group = GroupFactory(name=INTERNAL_GROUP_TRAINER, type=internal_group_type)
        cls.trainee_group = GroupFactory(name=INTERNAL_GROUP_TRAINEE, type=internal_group_type)
        cls.whoswho_group = GroupFactory(name=INTERNAL_GROUP_WHOSWHO, type=internal_group_type)

        # Create one job profile
        cls.job_profile = JobProfileFactory()

        # Create one business unit
        cls.business_unit = GroupFactory()

        # Create one administrator.
        cls.admin = AdminFactory()
        cls.admin.set_password(DEFAULT_PASSWORD)
        cls.admin.save()

        # The administrator is the manager of the job_profile
        GroupRoleFactory(
            user=cls.admin,
            group=cls.job_profile,
            role=GroupRole.ROLES.MANAGER
        )

        # The administrator is the manager of the trainer group.
        GroupRoleFactory(
            user=cls.admin,
            group=cls.business_unit,
            role=GroupRole.ROLES.MANAGER
        )

        # Create one trainer.
        cls.trainer = TrainerFactory()
        cls.trainer.set_password(DEFAULT_PASSWORD)
        cls.trainer.save()

        # The trainer is a member of the trainer group.
        GroupRoleFactory(user=cls.trainer, group=cls.business_unit)

        # Create and 3 trainees.
        cls.trainees = TraineeFactory.create_batch(3, password=DEFAULT_PASSWORD)
        cls.trainee = cls.trainees[0]

        # Create one theme
        cls.theme = ThemeFactory()

        # Create one training, associated with a business unit.
        cls.admin_training = TrainingFactory(
            owner=cls.admin,
            business_unit=cls.business_unit,
            job_profile=cls.job_profile,
            theme=cls.theme,
        )

        # Publish the training
        cls.admin_training.version_state = cls.admin_training.VERSION_STATES.PUBLISHED
        cls.admin_training.save()

        # Duplicate the training to the trainer
        cls.training = cls.admin_training.duplicate_to_members()[0]

        cls.training_session = TrainingSessionFactory(
            training=cls.training, trainer=cls.trainer
        )
        cls.training_session.users.add(cls.trainee)

        # Create the notice types
        NoticeType.create(
            "trainee_post_trainingsession",
            "Session content online",
            "Send to the trainee at the end of a training session"
        )
        NoticeType.create(
            "trainer_post_trainingsession",
            "Session content online",
            "Send to the trainer at the end of a training session"
        )
        NoticeType.create(
            "user_welcome",
            "Welcoming the user",
            "Send when a new user is registered across the system"
        )

        # TODO: Probably more things ;)

    def setUpMTPTestCase(self):
        """Add a django test client that will follow redirects.

        This method is called by ``setUp`` in subclasses.

        """

        # Get a test client.
        self.client = Client(follow=True)

    def assertListEqualUnordered(self, list1, list2):
        """Assert that two list have the same content, without taking the order into account.

        It is usefull when the entries cannot be used to create a set and use assertSetEqual.

        Parameters
        ----------
        list1 : list
            The first list (or other iterable) to compare
        list2 : list
            The second list (or other iterable) to compare

        """

        # Start by ensuring that both list have the same length
        self.assertEqual(len(list1), len(list2))

        # Then check that all items in list2 are in list1
        for entry in list2:
            self.assertIn(entry, list1)

        # And the reverse too (because we cannot be sure that two items are not exactly the same)
        for entry in list1:
            self.assertIn(entry, list2)


class MTPTestCase(BaseMTPTestCase, TestCase):
    """A test case based on ``BaseMTPTestCase`` and ``TestCase``.

    Should be used in most cases.

    """

    def setUp(self):
        super().setUp()

        # Additional test data created for each test case.
        self.setUpMTPTestCase()


class MTPTransactionTestCase(BaseMTPTestCase, TransactionTestCase):
    """A test case based on ``BaseMTPTestCase`` and ``TransactionTestCase``.

    Use this with parsimony, ``TransactionTestCase`` is a *lot* slower that default ``TestCase``.

    Notes
    -----
    In a ``TransactionTestCase``, the ``setUpTestData`` method is not called.
    So we call it in ``setUp``, which will recreate test data for each test.

    """

    def setUp(self):
        super().setUp()

        # Recreate default test data for each case.
        self.setUpTestData()

        # Additional test data created for each test case.
        self.setUpMTPTestCase()


class MTPBaseViewsTestCase:
    """A base test case providing a shortcut method to instantiate a view"""

    def get_view(self, view, user, *args, **kwargs):
        """Shortcut method to instantiate a view with the given args

        Parameters
        ----------
        view: An instance of the view we want to test
        user: instance of ``mtp.apps.accounts.models.MTPUser``
            The user to call the view with.
        args: list
            The args to call the view with
        kwargs: dict
            The kwargs to call the view with

        Returns
        -------
        An instance of the view

        """

        # Set all the needed properties of the view
        view.request = RequestFactory().get('/fake/path/not/used')
        view.request.user = user
        view.args, view.kwargs = args, kwargs

        return view


class MTPTemplateTestCaseMixin:
    """Mixin which provide a method to render template to test it."""

    @staticmethod
    def render_raw_template(raw_template, context=None):
        """Render the given raw template with an optional context.

        Parameters
        ----------
        raw_template : str
            The raw template to test.
            If you want to test a template tag, think to load it before with {% load `module` %}
        context : dict
            The context to render the template with.

        Returns
        -------
        str
            The template rendered.

        """

        context = Context(context or {})

        return Template(raw_template).render(context)

    @staticmethod
    def render_template(template, context=None):
        """Render the given template with an optional context.

        Parameters
        ----------
        template : django.template.backends.django.Template
            The backend template to test.
        context : dict
            The context to render the template with.

        Returns
        -------
        str
            The template rendered.

        """

        context = Context(context or {})

        return template.render(context)
