""".. Ignore pydocstyle D400.

==================
Resolwe Test Cases
==================

.. autoclass:: resolwe.test.TestCaseHelpers
    :members:

.. autoclass:: resolwe.test.TransactionTestCase
    :members:

.. autoclass:: resolwe.test.TestCase
    :members:

.. automodule:: resolwe.test.testcases.process

.. automodule:: resolwe.test.testcases.api

"""

from __future__ import absolute_import, division, print_function, unicode_literals

import copy
import os
import shutil

import six

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.test import SimpleTestCase as DjangoSimpleTestCase
from django.test import TestCase as DjangoTestCase
from django.test import TransactionTestCase as DjangoTransactionTestCase

from .setting_overrides import FLOW_EXECUTOR_SETTINGS


class TestCaseHelpers(DjangoSimpleTestCase):
    """Mixin for test case helpers."""

    def _pre_setup(self, *args, **kwargs):
        # NOTE: This is a work-around for Django issue #10827
        # (https://code.djangoproject.com/ticket/10827) that clears the
        # ContentType cache before permissions are setup.
        ContentType.objects.clear_cache()
        super()._pre_setup(*args, **kwargs)

    def _clean_up(self):
        """Clean up after test."""
        if not self._keep_data:
            # Do delete this here. See comment below near the makedirs
            # in setUp.
            shutil.rmtree(settings.FLOW_EXECUTOR['DATA_DIR'], ignore_errors=True)
            shutil.rmtree(settings.FLOW_EXECUTOR['UPLOAD_DIR'], ignore_errors=True)
            shutil.rmtree(settings.FLOW_EXECUTOR['RUNTIME_DIR'], ignore_errors=True)

    def setUp(self):
        """Prepare environment for test."""
        from resolwe.elastic.builder import index_builder

        super().setUp()

        # Prepare Elastic search indices.
        index_builder.discover_indexes()
        index_builder.create_mappings()
        index_builder.unregister_signals()
        index_builder.register_signals()

        # Directories need to be recreated here in case a previous
        # TestCase deleted them. Moving this logic into the test runner
        # and manager infrastructure would not work, because the manager
        # and listener can't know where the testcase boundaries are,
        # they just see a series of data objects; deleting too soon
        # might cause problems for some tests. The runner does not have
        # any code between tests, so could only remove data at the very
        # end, by which time it's already too late, since some tests may
        # deal specifically with the purging functionality and should
        # start in a clean environment, without the sediment
        # (e.g. jsonout.txt, stdout.txt) from previous tests.
        os.makedirs(FLOW_EXECUTOR_SETTINGS['DATA_DIR'], exist_ok=True)
        os.makedirs(FLOW_EXECUTOR_SETTINGS['UPLOAD_DIR'], exist_ok=True)
        os.makedirs(FLOW_EXECUTOR_SETTINGS['RUNTIME_DIR'], exist_ok=True)

        self._keep_data = settings.FLOW_MANAGER_KEEP_DATA

        self.addCleanup(self._clean_up)

    def tearDown(self):
        """Cleanup environment."""
        from resolwe.elastic.builder import index_builder

        super().tearDown()

        # Reset Elastic search indices.
        index_builder.destroy()

    def keep_data(self, mock_purge=True):
        """Do not delete output files after tests."""
        self.fail(
            "*ERROR* TestCaseHelpers.keep_data() is deprecated and does not work anymore.\n"
            "Using it will result in attribute errors in the future.\n"
            "Please use the command line options --keep-data and --no-mock-purge instead.\n"
        )

    def assertAlmostEqualGeneric(self, actual, expected, msg=None):  # pylint: disable=invalid-name
        """Assert almost equality for common types of objects.

        This is the same as :meth:`~unittest.TestCase.assertEqual`, but using
        :meth:`~unittest.TestCase.assertAlmostEqual` when floats are encountered
        inside common containers (currently this includes :class:`dict`,
        :class:`list` and :class:`tuple` types).

        :param actual: object to compare
        :param expected: object to compare against
        :param msg: optional message printed on failures
        """
        # pylint: disable=no-member
        self.assertEqual(type(actual), type(expected), msg=msg)

        if isinstance(actual, dict):
            six.assertCountEqual(self, actual.keys(), expected.keys(), msg=msg)
            for key in actual.keys():
                self.assertAlmostEqualGeneric(actual[key], expected[key], msg=msg)
        elif isinstance(actual, (list, tuple)):
            for actual_item, expected_item in zip(actual, expected):
                self.assertAlmostEqualGeneric(actual_item, expected_item, msg=msg)
        elif isinstance(actual, float):
            self.assertAlmostEqual(actual, expected, msg=msg)
        else:
            self.assertEqual(actual, expected, msg=msg)


class TransactionTestCase(TestCaseHelpers, DjangoTransactionTestCase):
    """Base class for writing Resolwe tests not enclosed in a transaction.

    It is based on Django's :class:`~django.test.TransactionTestCase`.
    Use it if you need to access the test's database from another
    thread/process.

    """

    def setUp(self):
        """Initialize test data."""
        super().setUp()

        user_model = get_user_model()
        self.admin = user_model.objects.create_superuser(username='admin', email='admin@test.com', password='admin')
        self.contributor = user_model.objects.create_user(username='contributor', email='contributor@test.com',
                                                          first_name='Joe', last_name='Miller')
        self.user = user_model.objects.create_user(username='normal_user', email='user@test.com')

        self.group = Group.objects.create(name='Users')
        self.group.user_set.add(self.user)


class TestCase(TransactionTestCase, DjangoTestCase):
    """Base class for writing Resolwe tests.

    It is based on :class:`~resolwe.test.TransactionTestCase` and
    Django's :class:`~django.test.TestCase`.
    The latter encloses the test code in a database transaction that is
    rolled back at the end of the test.

    """

    pass


# Deprecated legacy aliases.
TransactionElasticSearchTestCase = TransactionTestCase
ElasticSearchTestCase = TestCase
