"""This module holds the test cases for the managers and QuerySets of the ``trainings`` app"""

from collections import OrderedDict
from datetime import timedelta

from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from mtp.apps.accounts.constants import GROUPTYPE_BUSINESS_UNIT, GROUPTYPE_JOB_PROFILE
from mtp.apps.accounts.factories import (
    AdminFactory, GroupFactory,
    GroupTypeFactory, GroupRole,
    GroupRoleFactory, TrainerFactory
)
from mtp.apps.common.tests.test_cases import MTPTestCase
from mtp.apps.tasks.models import TASK_STATES

from ..factories import (
    TrainingActivityFactory, TrainingFactory,
    TrainingSessionFactory, TrainingTaskFactory
)
from ..models import Training, TrainingActivity, TrainingSession, TrainingTask, TASK_TYPES
from ..tasks import create_training_draft, duplicate_training_to_member


class TrainingSessionQuerySetVisibleByUserTestCase(MTPTestCase):
    """Test case for the ``visible_by_user`` method of the ``TrainingSessionQuerySet``"""

    @classmethod
    def setUpTestData(cls):
        """Set up the test data"""

        super().setUpTestData()

        job_profile_group_type = GroupTypeFactory(name=GROUPTYPE_JOB_PROFILE)
        job_profile = GroupFactory(type=job_profile_group_type)

        business_unit_group_type = GroupTypeFactory(name=GROUPTYPE_BUSINESS_UNIT)
        business_unit = GroupFactory(type=business_unit_group_type)

        # The first admin will be the job_profile manager
        GroupRoleFactory(
            user=cls.admin,
            group=job_profile,
            role=GroupRole.ROLES.MANAGER
        )

        # The second admin will be the business_unit manager
        cls.admin_2 = AdminFactory()
        GroupRoleFactory(
            user=cls.admin_2,
            group=business_unit,
            role=GroupRole.ROLES.MANAGER
        )

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

        # Admin 1 training
        admin_1_training = TrainingFactory(
            job_profile=job_profile
        )

        # Training session for the admin 1
        admin_1_training_session = TrainingSessionFactory(
            training=admin_1_training
        )

        # Admin 2 training
        admin_2_training = TrainingFactory(
            business_unit=business_unit,
            job_profile=job_profile
        )

        # Training session for the admin 2
        admin_2_training_session = TrainingSessionFactory(
            training=admin_2_training
        )

        # Trainer training, linked to the business unit
        trainer_training_business_unit = TrainingFactory(
            owner=cls.trainer,
            business_unit=business_unit,
            job_profile=job_profile
        )

        # Training session for the trainer, using this training
        trainer_training_session = TrainingSessionFactory(
            training=trainer_training_business_unit
        )

        # New "stand-alone" training (not linked with the business unit)
        trainer_training = TrainingFactory(owner=cls.trainer)

        # Training session for the trainer, using this training
        trainer_training_session_2 = TrainingSessionFactory(
            training=trainer_training
        )

        # Training session for the trainer...
        trainee_training_session = TrainingSessionFactory(
            training=trainer_training
        )

        # ...with one trainee
        trainee_training_session.users.add(cls.trainee)

        cls.visible_by_job_profile_manager = [
            admin_1_training_session,
            admin_2_training_session,
            trainer_training_session,
            cls.training_session,  # Coming from ``MTPTestCase``
        ]

        cls.visible_by_business_unit_manager = [
            admin_2_training_session,
            trainer_training_session,
        ]

        cls.visible_by_trainer = [
            trainer_training_session,
            trainer_training_session_2,
            trainee_training_session,
            cls.training_session,  # Coming from ``MTPTestCase``
        ]

        cls.visible_by_trainee_1 = [
            trainee_training_session,
            cls.training_session,  # Coming from ``MTPTestCase``
        ]

    def test_queryset_as_job_profile_manager(self):
        """Test the QuerySet result with the job_profile manager"""

        self.assertCountEqual(
            TrainingSession.objects.visible_by_user(self.admin),
            self.visible_by_job_profile_manager
        )

    def test_queryset_as_business_unit_manager(self):
        """Test the QuerySet result with the business_unit manager"""

        self.assertCountEqual(
            TrainingSession.objects.visible_by_user(self.admin_2),
            self.visible_by_business_unit_manager
        )

    def test_queryset_as_trainer(self):
        """Test the QuerySet result with the trainer user"""

        # Test with the first trainer
        self.assertCountEqual(
            TrainingSession.objects.visible_by_user(self.trainer),
            self.visible_by_trainer
        )

        # Test with another trainer, newly created
        self.assertCountEqual(
            TrainingSession.objects.visible_by_user(TrainerFactory()),
            []
        )

    def test_queryset_as_trainee(self):
        """Test the QuerySet result with the trainee user"""

        # Test with the first trainee
        self.assertCountEqual(
            TrainingSession.objects.visible_by_user(self.trainee),
            self.visible_by_trainee_1
        )

        # Test with the second trainee (not added to any session)
        self.assertCountEqual(
            TrainingSession.objects.visible_by_user(self.trainees[1]),
            []
        )


class TrainingTaskManagerTestCase(MTPTestCase):
    """Test the ``TrainingTaskManager`` manager"""

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

        # Create other trainings like the first one
        cls.admin_training2, cls.admin_training3, cls.admin_training4 = \
            TrainingFactory.create_batch(
                3,
                owner=cls.admin,
                business_unit=cls.business_unit,
                job_profile=cls.job_profile,
                theme=cls.theme,
            )

        # Create some fake tasks, we set the started date to know the order
        now = timezone.now()

        # Two duplicate for the second training
        cls.task1, cls.task2 = TrainingTaskFactory.create_batch(
            2,
            training=cls.admin_training2,
            user=cls.admin,
            task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
            started_at=now - timedelta(minutes=10)
        )
        # One draft for the second training
        cls.task3 = TrainingTaskFactory(training=cls.admin_training2, user=cls.admin,
                                        task_type=TASK_TYPES.CREATE_DRAFT,
                                        started_at=now - timedelta(minutes=8))
        # One duplicate for the fourth training
        cls.task4 = TrainingTaskFactory(training=cls.admin_training4, user=cls.admin,
                                        task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                                        started_at=now - timedelta(minutes=6))

        # One FINISHED duplicate for the fourth training
        TrainingTaskFactory(training=cls.admin_training4, user=cls.admin,
                            task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                            state=TASK_STATES.SUCCESS,
                            started_at=now - timedelta(minutes=4))

        # And a task for another user
        cls.task6 = TrainingTaskFactory(training=cls.training, user=cls.trainer,
                                        task_type=TASK_TYPES.CREATE_DRAFT,
                                        started_at=now - timedelta(minutes=2))

    def test_group_tasks_method(self):
        """Test the result of the ``group_tasks`` method of the manager """

        data = TrainingTask.objects.group_tasks(TrainingTask.objects.filter(
            user=self.admin,
            state__in=TASK_STATES.UNREADY_STATES.values
        ))

        # We should only see the waiting tasks for the user, and not the finished ones
        self.assertDictEqual(data, OrderedDict((
            (
                self.admin_training2.pk,
                {
                    'training': self.admin_training2,
                    'tasks': {
                        'DUPLICATE_TO_MEMBER': [self.task1, self.task2],
                        'CREATE_DRAFT': [self.task3],
                    }
                }
            ),
            (
                self.admin_training4.pk,
                {
                    'training': self.admin_training4,
                    'tasks': {
                        'DUPLICATE_TO_MEMBER': [self.task4],
                    }
                }
            ),
        )))

        # Check for the other user
        data = TrainingTask.objects.group_tasks(TrainingTask.objects.filter(
            user=self.trainer,
            state__in=TASK_STATES.UNREADY_STATES.values
        ))

        self.assertDictEqual(data, OrderedDict((
            (
                self.training.pk,
                {
                    'training': self.training,
                    'tasks': {
                        'CREATE_DRAFT': [self.task6],
                    }
                }
            ),
        )))

    def test_group_tasks_for_json_method(self):
        """Test the result of the ``group_tasks_for_json`` method of the manager """

        data = TrainingTask.objects.group_tasks_for_json(TrainingTask.objects.filter(
            user=self.admin,
            state__in=TASK_STATES.UNREADY_STATES.values
        ))

        # We should only see the waiting tasks for the user, and not the finished ones
        self.assertDictEqual(data, OrderedDict((
            (
                self.admin_training2.pk,
                {
                    'training_name': self.admin_training2.name,
                    'tasks': {
                        'DUPLICATE_TO_MEMBER': {'remaining': [self.task1.pk, self.task2.pk]},
                        'CREATE_DRAFT': {'remaining': [self.task3.pk]},
                    }
                }
            ),
            (
                self.admin_training4.pk,
                {
                    'training_name': self.admin_training4.name,
                    'tasks': {
                        'DUPLICATE_TO_MEMBER': {'remaining': [self.task4.pk]},
                    }
                }
            ),
        )))

        # Check for the other user
        data = TrainingTask.objects.group_tasks_for_json(TrainingTask.objects.filter(
            user=self.trainer,
            state__in=TASK_STATES.UNREADY_STATES.values
        ))

        self.assertDictEqual(data, OrderedDict((
            (
                self.training.pk,
                {
                    'training_name': self.training.name,
                    'tasks': {
                        'CREATE_DRAFT': {'remaining': [self.task6.pk]},
                    }
                }
            ),
        )))


class VersionableTrainingQuerySetTestCase(MTPTestCase):
    """Test the queryset used by the ``Training`` model"""

    @classmethod
    def setUpTestData(cls):
        """Add a second trainer"""
        super().setUpTestData()

        cls.new_trainer = TrainerFactory()

    def test_recently_created(self):
        """Test the ``recently_created`` method

        This test will check that:
        - old training is not in the list
        - very recent training not created by a task is in the list
        - not so recent training not created by a task is not in the list
        - very recent training created by a task for the user itself is in the list
        - not so recent training created by a task for the user itself is not in the list
        - recent training created by a task by another user is in the list

        """

        # The main trainer duplicate its training to another
        duplicate_training_to_member.delay(
            user=self.trainer,
            training=self.training,
            task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
            new_state=self.training.VERSION_STATES.PUBLISHED,
            params={
                'owner_id': self.new_trainer.pk,
                'name': 'duplicate',
            },
        )

        # Get the duplicated training
        new_training = Training.objects.get(parent_version=self.training, owner=self.new_trainer)

        # create another recent training for the new trainer, that should be in the recent list
        recent_training = TrainingFactory(
            owner=self.new_trainer,
            name='recent',
            version_state=Training.VERSION_STATES.PUBLISHED
        )

        # And make another draft from it, that should be in the recent list
        create_training_draft.delay(
            user=self.new_trainer,
            training=recent_training,
            task_type=TASK_TYPES.CREATE_DRAFT,
            fields={'name': 'recent draft'}
        )
        draft = Training.objects.get(parent_version=recent_training, owner=self.new_trainer)

        # Create another old training for the new trainer, that should not be in the recent list
        old_training = TrainingFactory(
            owner=self.new_trainer,
            name='old',
            created_at=timezone.now() - timedelta(days=10),
            version_state=Training.VERSION_STATES.PUBLISHED,
        )

        # And make another draft from it, recent but not enough to be in the list
        create_training_draft.delay(
            user=self.new_trainer,
            training=old_training,
            task_type=TASK_TYPES.CREATE_DRAFT,
            fields={'name': 'old draft'}
        )
        old_draft = Training.objects.get(parent_version=old_training, owner=self.new_trainer)
        old_draft.created_at = timezone.now() - timedelta(minutes=120)
        old_draft.save()

        # And another training not old but without task so enough old to be excluded from the list
        TrainingFactory(
            owner=self.new_trainer,
            name='not so recent',
            created_at=timezone.now() - timedelta(minutes=120),
        )

        # Get a queryset from which we want to extract the recent trainings
        trainings = Training.objects.filter(owner=self.new_trainer)

        # We can now extract the recent trainings from this list
        recent_trainings = trainings.recently_created(self.new_trainer)

        # We should have only the two recent ones, the last created on top
        self.assertEqual(recent_trainings, [
            draft,
            recent_training,
            new_training,
        ])

        # With their notice
        expected_notices = [
            str(_('New version you recently created')),
            str(_('Recently created')),
            str(_('Recently created by %(user)s') % {'user': self.trainer.get_full_name()}),
        ]
        self.assertListEqual(
            [str(training.notice) for training in recent_trainings],
            expected_notices
        )

    def test_get_duplicate_ids(self):
        """Test the ``get_duplicate_ids`` method"""

        # First case, with the existing trainings
        self.assertEqual(
            Training.objects.get_duplicate_ids(
                [self.admin_training.id]
            ),
            [self.training.id]
        )

        # 2 New trainings, children of "self.training"
        children_training_1 = TrainingFactory(
            parent_version=self.training,
        )
        # This one belongs to the admin and is a direct duplicate
        children_training_2 = TrainingFactory(
            parent_version=self.admin_training,
            owner=self.admin
        )

        self.assertCountEqual(
            Training.objects.get_duplicate_ids(
                [self.admin_training.id]
            ),
            [self.training.id, children_training_1.id, children_training_2.id]
        )

        # New training, children of "self.children_training"
        grand_children_training = TrainingFactory(
            parent_version=children_training_1
        )

        self.assertCountEqual(
            Training.objects.get_duplicate_ids(
                [self.admin_training.id]
            ),
            [
                self.training.id,
                children_training_1.id,
                children_training_2.id,
                grand_children_training.id
            ]
        )

        # Now, we exclude the trainings who belongs to the trainer
        self.assertCountEqual(
            Training.objects.get_duplicate_ids(
                [self.admin_training.id],
                exclude_owner_id=self.trainer.id
            ),
            [children_training_2.pk]
        )


class TrainingActivityQuerySetTestCase(MTPTestCase):
    """Test the queryset used by the ``TrainingActivity`` model"""

    @classmethod
    def setUpTestData(cls):
        """Add activities"""
        super().setUpTestData()

        cls.live_activity = TrainingActivityFactory(training=None, session=cls.training_session)
        cls.activity = TrainingActivityFactory(training=cls.training_session.training)

    def test_queryset_activity_for_session(self):
        """Test the QuerySet result with all activity, live and training"""
        self.assertIn(
            self.activity,
            TrainingActivity.objects.for_session(self.training_session)
        )

        self.assertIn(
            self.live_activity,
            TrainingActivity.objects.for_session(self.training_session)
        )

    def test_queryset_live_activity_for_training(self):
        """Test the QuerySet result with only training activity"""
        self.assertIn(
            self.activity,
            TrainingActivity.objects.for_training(self.training_session.training)
        )

        self.assertNotIn(
            self.live_activity,
            TrainingActivity.objects.for_training(self.training_session.training)
        )

    def test_annotate_is_duplicable_by(self):
        """Test the ``annotate_is_duplicable_by`` method

        A training is said ``duplicable`` when the current user is the manager
        of the business_unit of the training.

        """

        # Brand new training, owned by our ``self.trainer``
        trainer_training = TrainingFactory(
            owner=self.trainer,
            business_unit=None
        )

        # The training don't have any business unit, so it's not "duplicable"
        self.assertFalse(
            Training.objects.filter(pk=trainer_training.pk).annotate_is_duplicable_by(
                self.trainer
            ).first().is_duplicable
        )

        # Now, set a a business unit on the training
        trainer_training.business_unit = self.business_unit
        trainer_training.save()

        # The business unit is not managed by the trainer, so it's not "duplicable"
        self.assertFalse(
            Training.objects.filter(pk=trainer_training.pk).annotate_is_duplicable_by(
                self.trainer
            ).first().is_duplicable
        )

        # The admin manage the business unit, so the training should be flagged as "duplicable"
        self.assertTrue(
            Training.objects.filter(pk=trainer_training.pk).annotate_is_duplicable_by(
                self.admin
            ).first().is_duplicable
        )

    def test_available(self):
        """Test that the ``available``  method return only the available activities"""

        self.assertCountEqual(
            list(TrainingActivity.objects.available()),
            list(TrainingActivity.objects.instance_of(*Training.available_activity_models()))
        )
