"""Test the models and managers of the ``trainings`` app"""

from datetime import timedelta
from mock import call, patch

from django.core import mail
from django.core.exceptions import ValidationError
from django.test.utils import override_settings
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from mtp.apps.assessments.models import CompetencyTree
from mtp.apps.assessments.tests.test_management_commands import BaseComputeAssessmentScoresTestCase
from mtp.apps.brainstormers.factories import BrainstormerTemplateFactory
from mtp.apps.brainstormers.models import BrainstormerTemplate
from mtp.apps.broker_models_hooks.constants import CREATE, UPDATE
from mtp.apps.common.tests.test_cases import MTPTestCase
from mtp.apps.common.tests.waffle_test_utils import override_switch
from mtp.apps.common.utils import get_concrete_models, sort_list_of_dicts
from mtp.apps.documents.models import Document
from mtp.apps.roleplays.models import Roleplay
from mtp.apps.sync.models import Update
from mtp.apps.themes.models import Theme
from mtp.apps.workshops.factories import AssessingWorkshopFactory
from mtp.apps.workshops.models import Workshop

from mtp.discrete_apps.versionable.exceptions import NotDraftable, NotEditable

from ..factories import (
    TrainingActivityFactory,
    TrainingFactory,
    TrainingSessionFactory,
)
from ..models import (
    Training,
    TrainingActivity,
    TrainingSession,
    TrainingTask,
)
from ..redis_models import (
    Training as RedisTraining,
    TrainingSession as RedisTrainingSession
)

from .test_cases import TrainingWithActivitiesTestCase


class TrainingModelTestCase(TrainingWithActivitiesTestCase):
    """Test the ``Training`` model."""

    def test_get_members_to_duplicate_results(self):
        """Test the ``get_members_to_duplicate`` method."""

        to_duplicates = self.admin_training.get_members_to_duplicate()

        self.assertListEqualUnordered(to_duplicates, [
            # trainer owning a group (a "manager") receive a ``DRAFT``
            {
                'version_state': self.admin_training.VERSION_STATES.DRAFT,
                'owner': self.trainer_manager,
                'business_unit': self.business_unit_2
            },
            # trainer not owning a group receive a ``PUBLISHED``
            {
                'version_state': self.admin_training.VERSION_STATES.PUBLISHED,
                'owner': self.new_trainer,
                'business_unit': self.business_unit
            },
        ])

        # Create a training for one of these users (we simply create the training to simulate a
        # duplicate, which is a long operation)
        training = TrainingFactory(
            owner=self.new_trainer,
            business_unit=self.business_unit,
            parent_version=self.admin_training,
            original_version=self.admin_training.original_version,
        )
        training.version_state = training.VERSION_STATES.PUBLISHED
        training.save()

        # A new call to ``get_members_to_duplicate`` should only return one entry

        to_duplicates = self.admin_training.get_members_to_duplicate()

        self.assertListEqualUnordered(to_duplicates, [
            {
                'version_state': self.admin_training.VERSION_STATES.DRAFT,
                'owner': self.trainer_manager,
                'business_unit': self.business_unit_2
            },
        ])

        # Check with ``replace_published`` set to ``True``
        to_duplicates = self.admin_training.get_members_to_duplicate(replace_published=True)

        self.assertListEqualUnordered(to_duplicates, [
            # trainer owning a group (a "manager") receive a ``DRAFT``
            {
                'version_state': self.training.VERSION_STATES.DRAFT,
                'owner': self.trainer_manager,
                'business_unit': self.business_unit_2
            },
            # trainer not owning a group receive a ``PUBLISHED``, even if he already has one
            {
                'version_state': self.training.VERSION_STATES.PUBLISHED,
                'owner': self.new_trainer,
                'business_unit': self.business_unit
            },
            # We also have the original trainer (which received the training in a duplicate done
            # in `BaseMTPTestCase.setUpTestData`
            {
                'version_state': self.training.VERSION_STATES.PUBLISHED,
                'owner': self.trainer,
                'business_unit': self.business_unit
            },
        ])

    def test_get_members_to_duplicate_with_pks_results(self):
        """Test the ``get_members_to_duplicate`` method."""

        to_duplicates = self.admin_training.get_members_to_duplicate(use_pks=True)

        self.assertListEqualUnordered(to_duplicates, [
            # trainer owning a group (a "manager") receive a ``DRAFT``
            {
                'version_state': self.admin_training.VERSION_STATES.DRAFT,
                'owner_id': self.trainer_manager.pk,
                'business_unit_id': self.business_unit_2.pk
            },
            # trainer not owning a group receive a ``PUBLISHED``
            {
                'version_state': self.admin_training.VERSION_STATES.PUBLISHED,
                'owner_id': self.new_trainer.pk,
                'business_unit_id': self.business_unit.pk
            },
        ])

    def test_duplicate_to_member_not_async_should_call_create_new_version(self):
        """Test that the ``duplicate_to_member`` method works in synchronous mode."""

        # We'll only check if the ``create_new_version`` function is called
        with patch.object(Training, 'create_new_version') as patched:
            self.training.duplicate_to_member(
                new_state=self.training.VERSION_STATES.PUBLISHED,
                params={
                    'owner': self.new_trainer,
                    'business_unit': self.business_unit
                }
            )

        # We should have one call to ``Training.create_new_version``
        patched.assert_called_once_with(
            new_state=self.training.VERSION_STATES.PUBLISHED,
            params={
                'owner': self.new_trainer,
                'business_unit': self.business_unit,
            },
        )

    def test_duplicate_to_member_async_should_duplicate(self):
        """Test that the ``duplicate_to_member`` method works in asynchronous mode."""

        # No mock here to test the full path in async mode
        self.training.duplicate_to_member(
            new_state=self.training.VERSION_STATES.PUBLISHED,
            params={
                'owner': self.new_trainer,
                'business_unit': self.business_unit
            },
            async=True
        )

        # Get the training from the DB as the previous async call didn't return a training
        new_training = self.new_trainer.trainings.first()

        self.check_training(new_training, {
            'parent_version': self.training,
            'version_state': self.training.VERSION_STATES.PUBLISHED,
            'owner': self.new_trainer,
            'business_unit': self.business_unit,
        }, 6)

    def test_duplicate_to_member_should_not_archive_previous_training(self):
        """Test that the published training on non-manager trainers are archived after duplicate"""

        # Start by a normal duplicate to manager and simple trainer
        new_trainings = self.admin_training.duplicate_to_members()

        self.assertEqual(len(new_trainings), 2)
        for training in new_trainings:
            if training.owner == self.trainer_manager:
                manager_training = training
            elif training.owner == self.new_trainer:
                trainer_training = training
            else:
                self.fail('Unexpected training owner')

        # Publish the one for the manager, to check that it will still be published later
        manager_training.publish()

        # Ensure the status of the trainings are correct
        self.assertEqual(manager_training.version_state, Training.VERSION_STATES.PUBLISHED)
        self.assertEqual(trainer_training.version_state, Training.VERSION_STATES.PUBLISHED)

        # Now do the duplicate again, asking to archive
        new_trainings = self.admin_training.duplicate_to_members(replace_published=True)

        self.assertEqual(len(new_trainings), 2)
        for training in new_trainings:
            # We have one for the simple trainer
            if training.owner == self.new_trainer:
                trainer_training2 = training
            # And one for the original trainer, but we don't care about it
            elif training.owner == self.trainer:
                pass
            else:
                self.fail('Unexpected training owner')

        # Ensure the status of the new training for the simple trainer is correct
        self.assertEqual(trainer_training2.version_state, Training.VERSION_STATES.PUBLISHED)

        # Reload previous training
        manager_training.refresh_from_db()
        trainer_training.refresh_from_db()

        # And check their status
        self.assertEqual(manager_training.version_state, Training.VERSION_STATES.PUBLISHED)
        self.assertEqual(trainer_training.version_state, Training.VERSION_STATES.PUBLISHED)

    def test_duplicate_to_member_should_set_a_lock(self):
        """Test that the ``duplicate_to_member`` method set a lock."""

        params = {
            'owner': self.new_trainer,
            'business_unit': self.business_unit
        }

        # Ensure the lock is not set
        self.assertFalse(self.training.redis_instance.is_duplicate_to_member_locked(**params))

        # We prevent the lock to be released
        with patch.object(RedisTraining, 'release_duplicate_to_member_lock'):

            # We don't need the duplicate to really be done
            with patch.object(Training, 'create_new_version'):
                self.training.duplicate_to_member(
                    new_state=self.training.VERSION_STATES.PUBLISHED,
                    params=params
                )

        # Now we should have the lock set
        self.assertTrue(self.training.redis_instance.is_duplicate_to_member_locked(**params))

    def test_duplicate_to_member_should_stop_if_already_in_progress(self):
        """Test that we cannot run the same duplicate twice at the same time."""

        params = {
            'owner': self.new_trainer,
            'business_unit': self.business_unit
        }

        # Simulate another process running the same duplicate
        self.training.redis_instance.lock_duplicate_to_member(**params)

        # First in async mode, activating the exception
        with self.assertRaises(Training.DuplicateInProgress):
            self.training.duplicate_to_member(
                new_state=self.training.VERSION_STATES.PUBLISHED, params=params,
                async=True, skip_in_progress=False
            )

        # Then still in async mode, but skipping if in progress
        result = self.training.duplicate_to_member(
            new_state=self.training.VERSION_STATES.PUBLISHED, params=params,
            async=True, skip_in_progress=True
        )

        # Should return nothing if the duplicate was skipped
        self.assertIsNone(result)

        # Then in sync mode, activating the exception
        with self.assertRaises(Training.DuplicateInProgress):
            self.training.duplicate_to_member(
                new_state=self.training.VERSION_STATES.PUBLISHED, params=params,
                async=False, skip_in_progress=False
            )

        # Then still in sync mode, but skipping if in progress
        result = self.training.duplicate_to_member(
            new_state=self.training.VERSION_STATES.PUBLISHED, params=params,
            async=False, skip_in_progress=True
        )

        # Should return nothing if the duplicate was skipped
        self.assertIsNone(result)

    def test_duplicate_to_member_lock_is_released_in_case_of_errors(self):
        """Test that even if the duplicate fails, the lock is released."""

        params = {
            'owner': self.new_trainer,
            'business_unit': self.business_unit
        }

        with patch.object(Training, 'create_new_version', side_effect=RuntimeError('mocked!')):

            with self.assertRaises(RuntimeError):

                self.training.duplicate_to_member(
                    new_state=self.training.VERSION_STATES.PUBLISHED,
                    params=params
                )

        # Ensure the lock is removed
        self.assertFalse(self.training.redis_instance.is_duplicate_to_member_locked(**params))

    def test_duplicate_to_members_should_call_duplicate_to_member(self):
        """Test the ``duplicate_to_members`` method."""

        # Do a first duplicate to members

        # We'll only check if the ``duplicate_to_member`` method is called
        with patch.object(Training, 'duplicate_to_member') as patched:
            self.training.duplicate_to_members()

        # The method ``duplicate_to_member`` should have been called twice
        patched.assert_has_calls(
            [
                call(
                    new_state=self.training.VERSION_STATES.PUBLISHED,
                    params={
                        'owner': self.new_trainer,
                        'business_unit': self.business_unit,
                    },
                    async=False,
                    skip_in_progress=True,
                    author=None,
                ),
                call(
                    new_state=self.training.VERSION_STATES.DRAFT,
                    params={
                        'owner': self.trainer_manager,
                        'business_unit': self.business_unit_2,
                    },
                    async=False,
                    skip_in_progress=True,
                    author=None,
                ),
            ], any_order=True
        )

    @override_settings(INSTANCE_IS_MASTER=True)
    @override_switch('enable_duplicate_training_mail', active=True)
    def test_duplicate_to_member_should_send_a_mail(self):
        """Test the ``duplicate_to_member`` should send a mail, if applicable."""

        self.training.duplicate_to_member(
            new_state=self.training.VERSION_STATES.PUBLISHED,
            params={
                'owner': self.new_trainer,
                'business_unit': self.business_unit
            },
            async=False,
        )

        # We got one mail, for the trainer
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].to, [self.new_trainer.email])

    def test_create_draft_is_allowed_by_default(self):
        """Test that it's allowed to create a draft."""

        allowed, reason = self.training.is_new_draft_allowed()

        self.assertTrue(allowed)
        self.assertIsNone(reason)

    def test_create_draft_is_not_allowed_if_training_not_draftable(self):
        """Test that it's not allowed to create a draft for a non-draftable training."""

        # It's not allowed to create a draft for a training in `COPYING` state
        with self.training.override_version_state(self.training.VERSION_STATES.COPYING):
            allowed, reason = self.training.is_new_draft_allowed()

        self.assertFalse(allowed)
        self.assertIsInstance(reason, NotDraftable)

    def test_create_draft_is_not_allowed_if_draft_in_progress(self):
        """Test that it's not allowed to create a draft if one is in progress."""

        # Simulate a draft creation in progress
        self.training.redis_instance.lock_create_draft(owner=self.training.owner)

        allowed, reason = self.training.is_new_draft_allowed()

        self.assertFalse(allowed)
        self.assertIsInstance(reason, Training.DuplicateInProgress)

    def test_create_draft_should_call_create_new_version(self):
        """Test that the ``create_draft`` method works."""

        # We'll only check if the ``duplicate_training_to_member`` function is called
        with patch.object(Training, 'create_new_version') as patched:
            self.training.create_draft()

        # We should have one call to ``Training.create_new_version``
        patched.assert_called_once_with(
            new_state=self.training.VERSION_STATES.DRAFT,
            params={}
        )

    def test_create_draft_async_should_duplicate(self):
        """Test that the ``create_draft`` method works in asynchronous mode."""

        # Get count of trainings in draft state
        num_trainings_is_draft = self.trainer.trainings.filter_is_draft().count()

        # No mock here to test the full path in async mode
        self.training.create_draft(async=True)

        # One newly created draft
        self.assertEqual(
            self.trainer.trainings.filter_is_draft().count(),
            num_trainings_is_draft + 1
        )

        # Get the training from the DB as the previous async call didn't return a training
        new_training = self.trainer.trainings.filter_is_draft().first()

        self.check_training(new_training, {
            'parent_version': self.training,
            'version_state': self.training.VERSION_STATES.DRAFT,
            'owner': self.training.owner,
        }, 6)

    def test_create_draft_should_set_a_lock(self):
        """Test that the ``create_draft`` method set a lock."""

        params = {
            'owner': self.training.owner,
        }

        # Ensure the lock is not set
        self.assertFalse(self.training.redis_instance.is_create_draft_locked(**params))

        # We prevent the lock to be released
        with patch.object(RedisTraining, 'release_create_draft_lock'):

            # We don't need the duplicate to really be done
            with patch.object(Training, 'create_new_version'):
                self.training.create_draft()

        # Now we should have the lock set
        self.assertTrue(self.training.redis_instance.is_create_draft_locked(**params))

    def test_create_draft_should_stop_if_already_in_progress(self):
        """Test that we cannot create a draft twice at the same time."""

        params = {
            'owner': self.training.owner,
        }

        # Simulate another process running the same draft creation
        self.training.redis_instance.lock_create_draft(**params)

        # First in async mode, activating the exception
        with self.assertRaises(Training.DuplicateInProgress):
            self.training.create_draft(
                async=True, skip_in_progress=False
            )

        # Then still in async mode, but skipping if in progress
        result = self.training.create_draft(
            async=True, skip_in_progress=True
        )

        # Should return nothing if the duplicate was skipped
        self.assertIsNone(result)

        # Then in sync mode, activating the exception
        with self.assertRaises(Training.DuplicateInProgress):
            self.training.create_draft(
                async=False, skip_in_progress=False
            )

        # Then still in sync mode, but skipping if in progress
        result = self.training.create_draft(
            async=False, skip_in_progress=True
        )

        # Should return nothing if the draft creation was skipped
        self.assertIsNone(result)

    def test_create_draft_lock_is_released_in_case_of_errors(self):
        """Test that even if the draft creation fails, the lock is released."""

        params = {
            'owner': self.training.owner,
        }

        with patch.object(Training, 'create_new_version', side_effect=RuntimeError('mocked!')):

            with self.assertRaises(RuntimeError):
                self.training.create_draft()

        # Ensure the lock is removed
        self.assertFalse(self.training.redis_instance.is_create_draft_locked(**params))

    def test_is_assessing_property(self):
        """Test the ``is_assessing`` property."""

        # The training don't have any assessable activities
        self.assertFalse(self.training.is_assessing)

        AssessingWorkshopFactory(training=self.training)
        del self.training.is_assessing  # We invalidate the cache

        # Now, the training has an assessable activity
        self.assertTrue(self.training.is_assessing)

    def test_media_files(self):
        """Test that the ``media_files`` property should return the list of training media paths"""

        file_list = []

        # Add the media from the activities
        for activity in self.training.activities.non_deleted():
            if hasattr(activity, 'media_files'):
                file_list.extend(activity.media_files)

        # Add the media from the theme
        file_list.extend(self.training.get_theme().media_files)

        self.assertCountEqual(
            self.training.media_files,
            file_list
        )

    def test_external_id_property_when_set(self):
        """Test external_id property when set"""

        external_id = '2015-11-SALE-ASSESS'

        training_parent = TrainingFactory(
            _external_id=external_id
        )

        training_child = TrainingFactory(
            original_version=training_parent
        )

        self.assertEqual(external_id, training_child.external_id)

    def test_external_id_property_when_not_set(self):
        """Test external_id property when not set"""

        training_parent = TrainingFactory()

        training_child = TrainingFactory(
            original_version=training_parent
        )

        self.assertEqual('', training_child.external_id)

    def test_save_after_sync_should_work(self):
        """Test that saving the training after the whole synchronization still works

        It's a non-regression test. When written, it failed because the ``save`` method of the
        ``Syncable`` base model update the ``last_update`` field that was not in the
        ``non_lockable_fields``. But this update does something only after a synchronization
        because in this case a new ``Update`` object is created.

        """

        # Ensure the training is locked (in "published" state)
        self.assertEqual(self.training.version_state, self.training.VERSION_STATES.PUBLISHED)

        # will use the already existing ``Update`` object for ``last_update``
        self.training.save()

        # will set the ``synchronized`` field of the used``Update`` to ``True``
        Update.synchronize()

        # will create a new ``Update`` object, and will work because ``last_update`` is in the
        # ``non_lockable_fields``
        self.training.save()

    def test_archive_should_archive_the_training(self):
        """Test that ``archive`` method should archive the training"""

        self.admin_training.archive()

        # The admin training is archived
        self.assertEqual(self.admin_training.version_state, Training.VERSION_STATES.ARCHIVED)

        # The trainer training is not archived
        self.assertNotEqual(self.training.version_state, Training.VERSION_STATES.ARCHIVED)

    def test_archive_with_cascade_should_archive_the_training_and_the_children(self):
        """Test that the ``archive`` method should also archive the children"""

        self.admin_training.archive(cascade=True)

        # The admin training is archived
        self.assertEqual(self.admin_training.version_state, Training.VERSION_STATES.ARCHIVED)

        # The trainer training is also archived
        self.training.refresh_from_db()
        self.assertEqual(self.training.version_state, Training.VERSION_STATES.ARCHIVED)

    def test_archive_with_cascade_should_not_archive_the_children_owned_by_the_same_user(self):
        """Test that the ``archive`` method should not archive the children of the same user"""

        new_training_version = self.admin_training.create_draft(author=self.admin_training.owner)

        self.admin_training.archive(cascade=True)

        # The admin training is archived
        self.assertEqual(self.admin_training.version_state, Training.VERSION_STATES.ARCHIVED)

        # The trainer training is NOT archived
        self.assertEqual(new_training_version.version_state, Training.VERSION_STATES.DRAFT)

    def test_archive_with_cascade_and_not_commit_should_raise(self):
        """The ``archive`` method should raise if the "cascade" is True and "commit" is False"""

        with self.assertRaises(ValueError):
            self.admin_training.archive(cascade=True, commit=False)

    def test_default_theme_should_be_set_on_save(self):
        """Test that the ``theme`` field should be set when saving a new training"""

        training = Training(name='test', owner=self.admin)
        training.save()

        self.assertEqual(training.get_theme(), Theme.objects.get_default())


class TrainingSessionModelTestCase(MTPTestCase):
    """ The Tests case for the TrainingSession model """

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

        BrainstormerTemplateFactory.create_batch(3, training=cls.training)

        cls.session = TrainingSessionFactory(training=cls.training)

    def setUp(self):
        """Refresh data from db."""

        super().setUp()

        self.session.refresh_from_db()

    def test_new_session_should_create_brainstormer(self):
        """ Test if the creation of new session create brainstormer
        with templates in formation of this session """

        self.assertEqual(self.session.brainstormer_set.count(), 3)

    def test_old_session_save_should_not_create_brainstormer(self):
        """ Test if save of session not create new brainstormer
        with templates in formation of this session """

        self.session.save()

        self.assertEqual(self.session.brainstormer_set.count(), 3)

    @override_settings(INSTANCE_IS_MASTER=True)
    def test_close_session(self):
        """Test the ``close`` method on the ``TrainingSession`` model"""

        # The session state is ``NOT_YET_OPEN`` (by default)
        self.assertEqual(
            self.session.state,
            self.session.STATE_CHOICES.NOT_YET_OPEN
        )

        # Now, we close the session
        with patch.object(self.session, '_after_close') as after_close_patched:
            self.session.close()

        # The session state should now be ``CLOSED``
        self.session.refresh_from_db()
        self.assertEqual(
            self.session.state,
            self.session.STATE_CHOICES.CLOSED
        )

        # The _after_close method should have been called
        after_close_patched.assert_called_once_with(False)

        # Calling close again should not call ``_after_close``
        with patch.object(self.session, '_after_close') as after_close_patched:
            self.session.close()

        after_close_patched.assert_not_called()

    @override_settings(INSTANCE_IS_MASTER=False)
    def test_close_session_should_not_call__after_close_on_slave(self):
        """Test that the ``close`` method`` shouldn't call ``_after_close`` on SLAVE instance"""

        # The session state is ``NOT_YET_OPEN`` (by default)
        self.assertEqual(
            self.session.state,
            self.session.STATE_CHOICES.NOT_YET_OPEN
        )

        # Now, we close the session
        with patch.object(self.session, '_after_close') as after_close_patched:
            self.session.close()

        # The session state should now be ``CLOSED``
        self.session.refresh_from_db()
        self.assertEqual(
            self.session.state,
            self.session.STATE_CHOICES.CLOSED
        )

        # The _after_close method should not have been called
        after_close_patched.assert_not_called()

    def test_the_session_is_not_assessing(self):
        """Test the ``is_assessing`` property."""

        # In this case, the session is not assessing
        self.assertFalse(self.session.is_assessing)

    def test_open_session(self):
        """Test the ``open`` method."""

        # The session state is ``NOT_YET_OPEN`` (by default)
        self.assertEqual(
            self.session.state,
            self.session.STATE_CHOICES.NOT_YET_OPEN
        )

        # We open the session
        with patch.object(self.session, '_after_open') as after_open_patched:
            self.session.open()

        # The session state should now be ``OPEN``
        self.session.refresh_from_db()
        self.assertEqual(
            self.session.state,
            self.session.STATE_CHOICES.OPEN
        )

        # The _after_open method should have been called
        after_open_patched.assert_called_once_with()

        # Calling open again should not call ``_after_open``
        with patch.object(self.session, '_after_open') as after_open_patched:
            self.session.open()

        after_open_patched.assert_not_called()

        # We close the session
        with override_settings(INSTANCE_IS_MASTER=True):
            self.session.close()

        # We shouldn't be able to open a closed session
        with self.assertRaises(ValidationError):
            self.session.open()

    def test_is_live_should_work(self):
        """Test that the ``is_live`` property is working"""

        # An open session is always "live"
        self.assertEqual(self.training_session.state, TrainingSession.STATE_CHOICES.OPEN)
        self.assertTrue(self.training_session.is_live)

        # We close the session
        with override_settings(INSTANCE_IS_MASTER=True):
            self.training_session.close()

        # The session is not "live" anymore
        del self.training_session.is_live
        self.assertFalse(self.training_session.is_live)

    def test_adding_a_user_should_post_to_the_broker(self):
        """Test that adding a user into a session should post to the broker"""

        with patch('mtp.apps.broker_models_hooks.tasks.publish_to_broker.delay') as mocked:
            self.training_session.users.add(self.trainees[1])

        mocked.assert_called_once_with(
            self.trainees[1].get_serialized_representation(),
            self.trainees[1].get_trainingsession_broker_topic(self.training_session),
            'ADD'
        )

    def test_remove_a_user_should_post_to_the_broker(self):
        """Test that adding a user into a session should post to the broker"""

        with patch('mtp.apps.broker_models_hooks.tasks.publish_to_broker.delay') as mocked:
            self.training_session.users.remove(self.trainee)

        mocked.assert_called_once_with(
            self.trainee.get_serialized_representation(),
            self.trainee.get_trainingsession_broker_topic(self.training_session),
            'REMOVE'
        )

    def test_average_tablet_usage_time_is_always_valid(self):
        """Test that we not try to save an invalid ``average_tablet_usage_time`` value"""

        # set None: it must still be None
        self.training_session.average_tablet_usage_time = None
        self.training_session.save()
        self.training_session.refresh_from_db()
        self.assertIsNone(self.training_session.average_tablet_usage_time)

        # set a valid value
        self.training_session.average_tablet_usage_time = 5
        self.training_session.save()
        self.training_session.refresh_from_db()
        self.assertEqual(self.training_session.average_tablet_usage_time, 5)

        # set an invalid value: it must be set to 0
        self.training_session.average_tablet_usage_time = -5
        self.training_session.save()
        self.training_session.refresh_from_db()
        self.assertEqual(self.training_session.average_tablet_usage_time, 0)


class AssessingTrainingSessionModelTestCase(BaseComputeAssessmentScoresTestCase):
    """Tests case for the TrainingSession model when is used with assessment."""

    def test_the_session_is_assessing(self):
        """Test the ``is_assessing`` property."""

        # In this case, the session is assessing
        self.assertTrue(self.training_session.is_assessing)

    def test_compute_assessment_scores(self):
        """Test the ``compute_assessment_scores`` method."""

        with self.assertSessionRecomputed():
            self.training_session.compute_assessment_scores()

    def test_compute_assessment_score_should_acquire_the_lock(self):
        """Test that the ``compute_assessment_scores`` should acquire the lock"""

        with patch.object(RedisTrainingSession,
                          'acquire_assessment_computation_lock',
                          return_value=True) as mocked_lock:
            with patch.object(CompetencyTree, 'compute_scores') as mocked_compute:
                self.training_session.compute_assessment_scores()

        mocked_lock.assert_called_once_with()
        mocked_compute.assert_called_once_with(
            self.trainee,
            self.training_session,
            force_recompute=False
        )

    def test_compute_assessment_score_should_not_be_done_if_lock_is_set(self):
        """Test that the ``compute_assessment_scores`` should not compute if lock is set"""

        with patch.object(RedisTrainingSession,
                          'acquire_assessment_computation_lock',
                          return_value=False):
            with patch.object(CompetencyTree, 'compute_scores') as mocked_compute:
                self.training_session.compute_assessment_scores()

        # The computation wasn't done because the lock couldn't be acquired
        mocked_compute.assert_not_called()

    def test_close_session(self):
        """Test the ``close`` method compute assessment scores."""

        self.assertFalse(self.training_session.is_closed)

        # The competency tree score is already computed in ``setUp`` method.
        # We don't want force recomputed it because we don't have ``AssessmentPartScore``
        with patch.object(CompetencyTree, 'compute_scores', return_value=None):
            with self.assertSessionRecomputed():
                with override_settings(INSTANCE_IS_MASTER=True):
                    self.training_session.close()

        self.assertTrue(self.training_session.is_closed)


class TrainingSessionManagerTestCase(MTPTestCase):
    """Test cases for the ``TrainingSessionManager`` class"""

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

        super().setUpTestData()

        # One "outdated" session (with a ``date_end`` in the past)
        cls.outdated_session = TrainingSessionFactory(
            end_date=timezone.now() - timedelta(days=1)
        )
        # One "during" session (with a ``date_end`` in the future)
        cls.during_session = TrainingSessionFactory(
            end_date=timezone.now() + timedelta(days=1)
        )

    @override_settings(INSTANCE_IS_MASTER=True)
    def test_close_outdated_with_no_grace_period(self):
        """Test the ``close_outdated`` method with ``days_after`` at 0"""

        # We got no ``CLOSED`` sessions
        self.assertEqual(
            TrainingSession.objects.closed().count(),
            0
        )

        # Call the method
        result = TrainingSession.objects.close_outdated()

        # We got two results, because of the the one from ``MTPTestCase``
        self.assertCountEqual(
            result,
            [self.outdated_session.pk, self.training_session.pk]
        )

        # We got 2 ``CLOSED`` sessions
        self.assertEqual(
            TrainingSession.objects.closed().count(),
            2
        )

        # Call again the method
        result = TrainingSession.objects.close_outdated()

        # We got no results, the sessions were already closed
        self.assertListEqual(
            result,
            []
        )

        # Modify the "during_session" to make it "outdated"
        self.during_session.end_date = timezone.now() - timedelta(days=1)
        self.during_session.save()

        # Call again the method
        result = TrainingSession.objects.close_outdated()

        # We got one result
        self.assertListEqual(
            result,
            [self.during_session.pk]
        )

        # We got now 3 ``CLOSED`` sessions
        self.assertEqual(
            TrainingSession.objects.closed().count(),
            3
        )

    @override_settings(INSTANCE_IS_MASTER=True)
    def test_close_outdated_with_a_grace_period(self):
        """Test the ``close_outdated`` method with ``days_after`` at 1"""

        # We got no ``CLOSED`` sessions
        self.assertEqual(
            TrainingSession.objects.closed().count(),
            0
        )

        # Call the method with 3 day of "grace period"
        result = TrainingSession.objects.close_outdated(days_after=3)

        # We got no results, as there are no sessions who ended three days ago (or before)
        self.assertListEqual(
            result,
            []
        )

    @override_settings(INSTANCE_IS_MASTER=True)
    def test_close_outdated_without_dry_run(self):
        """Test the ``close_outdated`` method with ``dry_run`` to ``False``"""

        # We got no ``CLOSED`` sessions
        self.assertEqual(
            TrainingSession.objects.closed().count(),
            0
        )

        # We check that the ``close`` method is called
        with patch.object(TrainingSession, 'close', autospec=True) as patched:
            TrainingSession.objects.close_outdated(dry_run=False)

        # The close method was called twice
        self.assertEqual(
            patched.call_count,
            2
        )

    @override_settings(INSTANCE_IS_MASTER=True)
    def test_close_outdated_with_dry_run(self):
        """Test the ``close_outdated`` method with ``dry_run`` to ``True``"""

        # We got no ``CLOSED`` sessions
        self.assertEqual(
            TrainingSession.objects.closed().count(),
            0
        )

        # We check that the ``close`` method is not called
        with patch.object(TrainingSession, 'close') as patched:
            result = TrainingSession.objects.close_outdated(dry_run=True)

        # We got two results, because of the the one from ``MTPTestCase``
        self.assertCountEqual(
            result,
            [self.outdated_session.pk, self.training_session.pk]
        )

        # But the close method was not called...
        patched.assert_not_called()

        # ..and we still got no ``CLOSED`` sessions
        self.assertEqual(
            TrainingSession.objects.closed().count(),
            0
        )

    def test_get_assessing_session(self):
        """Test the ``get_assessing_session``."""

        # We have no assessing session because no assessing activity
        assessing_session = TrainingSession.objects.get_assessing_sessions()
        self.assertEqual(len(assessing_session), 0)

        # We add an assessing activity in current session training
        TrainingActivityFactory(training=self.training_session.training, is_assessing=True)

        # Now, we should have 1 assessing session, the current
        assessing_session = TrainingSession.objects.get_assessing_sessions()
        self.assertEqual(len(assessing_session), 1)
        self.assertTrue(self.training_session in assessing_session)


class TrainingTaskTestCase(MTPTestCase):
    """Test case for the ``TrainingTask`` model"""

    def test_notice(self):
        """Test the return value of ``get_finished_notice_for_user``"""

        task_draft = TrainingTask(
            task_type=TrainingTask.TASK_TYPES.CREATE_DRAFT,
            user=self.trainer
        )
        task_duplicate = TrainingTask(
            task_type=TrainingTask.TASK_TYPES.DUPLICATE_TO_MEMBER,
            user=self.trainer
        )

        # A task seen by it's owner

        # A draft
        self.assertEqual(
            task_draft.get_finished_notice_for_user(self.trainer),
            _('New version you recently created')
        )
        # A duplicate
        self.assertEqual(
            task_duplicate.get_finished_notice_for_user(self.trainer),
            _('Recently created by you')
        )

        # A task seen by other

        # A draft
        self.assertEqual(
            task_draft.get_finished_notice_for_user(self.trainee),
            _('Recently created by %(user)s') % {'user': self.trainer.get_full_name()}
        )
        # A duplicate
        self.assertEqual(
            task_duplicate.get_finished_notice_for_user(self.trainee),
            _('Recently created by %(user)s') % {'user': self.trainer.get_full_name()}
        )


class TrainingActivityModelTestCase(MTPTestCase):
    """ The Tests case for the TrainingActivity model """

    def setUp(self):
        """Define test data """

        super().setUp()

        self.draft_training = TrainingFactory()

    def test_live_activity(self):
        """Test the is_live property defined by the live activity mixin"""

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

        self.assertEqual(
            live_activity.is_live,
            True
        )

        self.assertEqual(
            activity.is_live,
            False
        )

    def test_available_activity_models(self):
        """Test that the ``available_activity_models`` method is working correctly"""

        deactivated_activities = [Document, Roleplay, Workshop, BrainstormerTemplate]

        expected_activity_models = [
            cls for cls in get_concrete_models(TrainingActivity)
            if cls not in deactivated_activities
        ]

        self.assertEqual(
            TrainingActivity.available_activity_models(),
            expected_activity_models
        )

    def test_available_activity_types(self):
        """Test that the ``available_activity_types`` is working correctly"""

        self.assertEqual(
            TrainingActivity.available_activity_types(),
            sort_list_of_dicts([
                {
                    'create_url': activity_cls.get_create_url(),
                    'verbose_name': activity_cls.verbose_activity_name,
                    'type': activity_cls.__name__.lower(),
                }
                for activity_cls in TrainingActivity.available_activity_models()
            ], 'verbose_name')
        )

    def test_available_activity_types_with_training(self):
        """Test that the ``available_activity_types`` with a training is working correctly"""

        self.assertEqual(
            TrainingActivity.available_activity_types(self.training),
            sort_list_of_dicts([
                {
                    'create_url': activity_cls.get_training_create_url(training=self.training),
                    'verbose_name': activity_cls.verbose_activity_name,
                    'type': activity_cls.__name__.lower(),
                }
                for activity_cls in TrainingActivity.available_activity_models()
            ], 'verbose_name')
        )

    def test_save_raise_error_linked_to_training_and_session(self):
        """Test save action raise an error if activity is :
        - linked to training and to a session at the same time
        - not linked to a training neither to a session

        """

        # We can not save an activity when activity is linked to a training and a session at the
        # same time
        error_msg = 'The activity can not be linked to a training and to a session at the same time'
        with self.assertRaisesMessage(TypeError, error_msg):
            TrainingActivityFactory(
                training=self.training_session.training,
                session=TrainingSessionFactory()
            )

        # We can not save an activity when activity is NOT linked to a training and neither to
        # a session
        error_msg = 'The activity has to be linked to a training, to a session ' \
                    'or to a library status'
        with self.assertRaisesMessage(TypeError, error_msg):
            TrainingActivityFactory(training=None, session=None)

        # Finally we can save with no session
        TrainingActivityFactory(training=self.training_session.training, session=None)

        # Or with no training
        TrainingActivityFactory(training=None, session=TrainingSessionFactory())

    def test_update_activity(self):
        """Test activity could be updated"""

        live_activity = TrainingActivityFactory(training=None, session=self.training_session)
        activity_published = TrainingActivityFactory(training=self.training_session.training)
        activity_draft = TrainingActivityFactory(training=self.draft_training)

        # we can update live activity
        live_activity.name = 'New activity name'

        # But we can NOT update when the training is published
        with self.assertRaises(NotEditable):
            activity_published.name = 'New activity name'

        # Finally we can update when the training is a draft
        activity_draft.name = 'New activity name'

    def test_unique_together_constraint(self):
        """Test activity (sort_oder, training, session) constraint

        The sort_order depends on training AND session
        """

        activity = TrainingActivityFactory(training=self.draft_training)
        activity2 = TrainingActivityFactory(training=self.draft_training)
        activity_new = TrainingActivityFactory()
        live_activity = TrainingActivityFactory(training=None, session=self.training_session)
        live_activity2 = TrainingActivityFactory(training=None, session=self.training_session)
        live_activity_new = TrainingActivityFactory(training=None, session=TrainingSessionFactory())

        # By Training
        # For an activity link to a training we set the same order
        activity.sort_order = 1
        activity.save()
        activity2.sort_order = 1
        activity2.save()

        # we refresh data
        activity.refresh_from_db()
        activity2.refresh_from_db()

        # then the first activity updated, should have its sort_update incremented
        self.assertEqual(activity.sort_order, 2)
        self.assertEqual(activity2.sort_order, 1)

        # Now we update the sort_order on a activity link to an other training
        activity_new.sort_order = 1
        activity_new.save()

        # we refresh data
        activity_new.refresh_from_db()
        activity.refresh_from_db()
        activity2.refresh_from_db()

        # The sort_order is 1
        self.assertEqual(live_activity_new.sort_order, 1)

        # and the order of other activities linked to training should not change
        self.assertEqual(activity.sort_order, 2)
        self.assertEqual(activity2.sort_order, 1)

        # By session
        # For an activity link to a session we set the same order
        live_activity.sort_order = 1
        live_activity.save()
        live_activity2.sort_order = 1
        live_activity2.save()

        # we refresh data
        activity.refresh_from_db()
        activity2.refresh_from_db()
        live_activity.refresh_from_db()
        live_activity2.refresh_from_db()

        # then the first activity updated, should have its sort_update incremented
        self.assertEqual(live_activity.sort_order, 2)
        self.assertEqual(live_activity2.sort_order, 1)
        # and the order of other activities linked to training should not change
        self.assertEqual(activity.sort_order, 2)
        self.assertEqual(activity2.sort_order, 1)

        # Now we update the sort_order on a activity link to an other session
        live_activity_new.sort_order = 1
        live_activity_new.save()

        # we refresh data
        activity.refresh_from_db()
        activity2.refresh_from_db()
        live_activity.refresh_from_db()
        live_activity2.refresh_from_db()
        live_activity_new.refresh_from_db()

        # The sort_order is 1
        self.assertEqual(live_activity_new.sort_order, 1)

        # and the order of other activities linked to training or other sessions should not change
        self.assertEqual(activity.sort_order, 2)
        self.assertEqual(activity2.sort_order, 1)
        self.assertEqual(live_activity.sort_order, 2)
        self.assertEqual(live_activity2.sort_order, 1)

    def test_should_be_sent_to_broker(self):
        """Test that the ``should_be_sent_to_broker`` method should work correctly"""

        # New activity linked to a training
        training_activity = TrainingActivityFactory(
            training=self.training,
            session=None
        )

        self.assertFalse(training_activity.should_be_sent_to_broker(CREATE))
        self.assertFalse(training_activity.should_be_sent_to_broker(UPDATE))

        # New activity linked to a session
        session_activity = TrainingActivityFactory(
            training=None,
            session=self.training_session
        )

        self.assertTrue(session_activity.should_be_sent_to_broker(CREATE))
        self.assertFalse(session_activity.should_be_sent_to_broker(UPDATE))

        # With a closed session
        del self.training_session.is_live
        self.training_session.close()
        self.assertFalse(session_activity.should_be_sent_to_broker(CREATE))

    def test_get_serialized_representation(self):
        """Test that the ``get_serialized_representation`` method should work correctly"""

        # New activity linked to a session
        session_activity = TrainingActivityFactory(
            training=None,
            session=self.training_session
        )

        self.assertEqual(
            session_activity.get_serialized_representation(),
            {
                'id': session_activity.id,
                'name': session_activity.name,
                'description': session_activity.description,
                'activity_type': session_activity.activity_type,
                'sort_order': session_activity.sort_order,
                'session': session_activity.session_id,
                'training': session_activity.training_id
            }
        )

    def test_new_session_activity_should_be_sent_to_the_broker(self):
        """Test that a newly created session activity should be sent to the broker"""

        with patch('mtp.apps.broker_models_hooks.tasks.publish_to_broker.delay') as mocked:
            session_activity = TrainingActivityFactory(
                training=None,
                session=self.training_session
            )

        mocked.assert_called_once_with(
            session_activity.get_serialized_representation(),
            session_activity.get_broker_topic(),
            CREATE
        )


class TrainingActivityDuplicateTestCase(TrainingWithActivitiesTestCase):
    """Test case for the duplicate behaviour of the ``TrainingActivity`` model"""

    def test_duplicate_in_training_should_duplicate_an_activity_in_the_targeted_training(self):
        """Test that we should be able to duplicate an activity in a targeted training"""

        # Create a new ``Training``
        new_training = TrainingFactory()

        # This training don't have any activities (yet)
        self.assertEqual(
            new_training.activities.count(),
            0
        )

        first_activity = self.training.activities.first()
        new_activity = first_activity.duplicate_in_training(training=new_training)

        # Our new activity should be associated with our new training...
        self.assertEqual(new_activity.training, new_training)

    def test_duplicate_in_training_should_reset_sort_order(self):
        """Test that duplicated an activity in a training should reset its sort_order"""

        # Create a new ``Training``
        new_training = TrainingFactory()

        last_activity = self.training.activities.last()

        duplicated_activity = last_activity.duplicate_in_training(training=new_training)

        # Since it is the first activity of this training, the sort_order should be 1
        self.assertEqual(duplicated_activity.sort_order, 1)

        # Duplicate a second time the activity
        second_duplicated_activity = last_activity.duplicate_in_training(training=new_training)

        # The sort order should be 2 for this one
        self.assertEqual(second_duplicated_activity.sort_order, 2)

    def test_duplicate_in_library_should_duplicate_an_activity_in_the_user_library(self):
        """Test that we should be able to duplicate an activity in the user library"""

        first_activity = self.training.activities.first()
        new_activity = first_activity.duplicate_in_library(self.trainer)

        # Our new activity should be...
        self.assertIsNone(new_activity.training)  # Not associated with a training
        self.assertEqual(new_activity.library_status.owner, self.trainer)  # Owned by our user
        self.assertFalse(new_activity.library_status.is_shared)  # NOT Shared in the public library

    def test_duplicate_in_library_should_duplicate_an_activity_in_the_public_library(self):
        """Test that we should be able to duplicate an activity in the public library"""

        first_activity = self.training.activities.first()
        new_activity = first_activity.duplicate_in_library(self.trainer, is_shared=True)

        # Our new activity should be...
        self.assertIsNone(new_activity.training)  # Not associated with a training
        self.assertEqual(new_activity.library_status.owner, self.trainer)  # Owned by our user
        self.assertTrue(new_activity.library_status.is_shared)  # Shared in the public library
