"""Test the tasks of the ``trainings`` app"""

import pickle
from datetime import timedelta

from mock import patch

from constance.test import override_config

from mtp.apps.assessments.tests.test_management_commands import BaseComputeAssessmentScoresTestCase
from mtp.apps.common.tests.test_cases import MTPTestCase

from ..models import Training, TrainingSession, TrainingTask, TASK_TYPES
from ..tasks import (
    archive_training,
    compute_assessment_scores,
    close_outdated_training_sessions,
    create_training_draft,
    duplicate_training_to_member,
)

from .test_cases import BaseTrainingTestCase


class VersionableTrainingModelTestCase(BaseTrainingTestCase):
    """The tests for the versionable behavior of the training model. """

    def test_duplicate_training_to_member_should_work(self):
        """Test the behavior of the ``duplicate_training_to_member`` task."""

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

            # Arguments will be updated by ``TrainingTask``
            # pylint: disable=unexpected-keyword-arg,no-value-for-parameter
            duplicate_training_to_member(
                user=None,
                training=self.training,
                task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                new_state=self.training.VERSION_STATES.PUBLISHED,
                params={
                    'owner_id': self.new_trainer.pk,
                    'business_unit_id': self.business_unit.pk
                },
            )

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

    def test_duplicate_training_to_member_should_create_task_instance(self):
        """An instance of ``TrainingTask`` should be created when the task is executed"""

        previous_count = TrainingTask.objects.count()

        result = 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,
                'business_unit_id': self.business_unit.pk
            },
        )

        # We should have one more task in db
        self.assertEqual(TrainingTask.objects.count(), previous_count + 1)

        # So we can check its content
        task = TrainingTask.objects.last()
        self.assertEqual(str(task.celery_task_id), str(result.task_id))
        self.assertEqual(task.user_id, self.trainer.pk)
        self.assertEqual(task.training_id, self.training.pk)
        self.assertEqual(task.task_type, TASK_TYPES.DUPLICATE_TO_MEMBER)
        self.assertEqual(task.state, task.TASK_STATES.SUCCESS)
        self.assertEqual(task.created_training_id, result.result)
        self.assertEqual(task.created_training, Training.objects.get(pk=result.result))

    def test_duplicate_training_to_member_should_be_skipped_if_in_progress(self):
        """Test the behavior of the ``duplicate_training_to_member`` task if already in progress."""

        # Simulate another process running the same duplicate
        self.training.redis_instance.lock_duplicate_to_member(
            owner=self.new_trainer,
            business_unit=self.business_unit
        )

        # Launch the task

        # Arguments will be updated by ``TrainingTask``
        # pylint: disable=unexpected-keyword-arg,no-value-for-parameter
        result = duplicate_training_to_member(
            user=None,
            training=self.training,
            task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
            new_state=self.training.VERSION_STATES.PUBLISHED,
            params={
                'owner_id': self.new_trainer.pk,
                'business_unit_id': self.business_unit.pk
            },
        )

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

    def test_duplicate_to_members_params_are_picklable(self):
        """Test that the data received by the delay method are picklable."""

        # We don't really call the duplicate
        with patch.object(duplicate_training_to_member, 'delay') as patched:
            self.training.duplicate_to_members(async=True)

        # It will raise if args/kwargs are not picklable
        # We use protocol 2 as in celery
        # (cf http://docs.celeryproject.org/projects/kombu/en/latest/userguide/serialization.html)
        for args_list in patched.call_args_list:
            pickle.dumps(args_list[0], protocol=2)  # args
            pickle.dumps(args_list[1], protocol=2)  # kwargs

    def test_duplicate_to_members_result_is_picklable(self):
        """Test that the data returned by the duplicate method is picklable."""

        result = duplicate_training_to_member.delay(
            user=None,
            training=self.training,
            task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
            new_state=self.training.VERSION_STATES.PUBLISHED,
            params={
                'owner_id': self.new_trainer.pk,
                'business_unit_id': self.business_unit.pk
            },
        )

        pickle.dumps(result, protocol=2)

    def test_create_training_draft_should_work(self):
        """Test the behavior of the ``create_training_draft`` task."""

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

            # Arguments will be updated by ``TrainingTask``
            # pylint: disable=unexpected-keyword-arg,no-value-for-parameter
            create_training_draft(
                user=None,
                training=self.training,
                task_type=TASK_TYPES.CREATE_DRAFT,
            )

        # We should have one call to ``Training.create_draft``
        patched.assert_called_once_with(
            async=False,
            skip_in_progress=False,
        )

    def test_create_training_draft_should_create_task_instance(self):
        """An instance of ``TrainingTask`` should be created when the task is executed"""

        previous_count = TrainingTask.objects.count()

        result = create_training_draft.delay(
            user=self.trainer,
            training=self.training,
            task_type=TASK_TYPES.CREATE_DRAFT,
        )

        # We should have one more task in db
        self.assertEqual(TrainingTask.objects.count(), previous_count + 1)

        # So we can check its content
        task = TrainingTask.objects.last()
        self.assertEqual(str(task.celery_task_id), str(result.task_id))
        self.assertEqual(task.user_id, self.trainer.pk)
        self.assertEqual(task.training_id, self.training.pk)
        self.assertEqual(task.task_type, TASK_TYPES.CREATE_DRAFT)
        self.assertEqual(task.state, task.TASK_STATES.SUCCESS)
        self.assertEqual(task.created_training_id, result.result)
        self.assertEqual(task.created_training, Training.objects.get(pk=result.result))

    def test_create_training_draft_should_be_skipped_if_in_progress(self):
        """Test the behavior of the ``create_training_draft`` task if already in progress."""

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

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

        # Launch the task

        # Arguments will be updated by ``TrainingTask``
        # pylint: disable=unexpected-keyword-arg, no-value-for-parameter
        result = create_training_draft(
            user=None,
            training=self.training,
            task_type=TASK_TYPES.CREATE_DRAFT,
        )

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

    def test_create_draft_params_are_picklable(self):
        """Test that the data received by the delay method are picklable."""

        # We don't really call the duplicate
        with patch.object(create_training_draft, 'delay') as patched:
            self.training.create_draft(async=True)

        # It will raise if args/kwargs are not picklable
        # We use protocol 2 as in celery
        # (cf http://docs.celeryproject.org/projects/kombu/en/latest/userguide/serialization.html)
        for args_list in patched.call_args_list:
            pickle.dumps(args_list[0], protocol=2)  # args
            pickle.dumps(args_list[1], protocol=2)  # kwargs

    def test_create_draft_result_is_picklable(self):
        """Test that the data returned by the duplicate method is picklable."""

        result = create_training_draft.delay(
            user=None,
            training=self.training,
            task_type=TASK_TYPES.CREATE_DRAFT,
        )

        pickle.dumps(result, protocol=2)


class ComputeAssessmentScoresTaskTestCase(BaseComputeAssessmentScoresTestCase):
    """Test cases for the ``compute_assessment_scores`` task."""

    def test_computation_works_with_force_recompute(self):
        """Test the computation works with force recompute."""

        # We just need to test if the task call the ``compute_assessment_scores`` method.
        with patch.object(TrainingSession, 'compute_assessment_scores') as patched:
            compute_assessment_scores(self.training_session.pk, force_recompute=True)

        # The method should be called once, with default args
        patched.assert_called_once_with(force_recompute=True)

    def test_computation_works_without_force_recompute(self):
        """Test the computation works without force recompute."""

        # We just need to test if the task call the ``compute_assessment_scores`` method.
        with patch.object(TrainingSession, 'compute_assessment_scores') as patched:
            compute_assessment_scores(self.training_session.pk, force_recompute=False)

        # The method should be called once, with default args
        patched.assert_called_once_with(force_recompute=False)


class TrainingSessionCloseOutdatedTestCase(MTPTestCase):
    """Test cases for the ``close_outdated_training_sessions`` task"""

    def test_close_outdated_training_sessions(self):
        """Test the ``close_outdated_training_sessions`` task"""

        # We just need to test if the task call the ``close_outdated`` method.
        with patch.object(TrainingSession.objects, 'close_outdated') as patched:
            close_outdated_training_sessions()

        # The manager method was called once, with default args
        patched.assert_called_once_with(days_after=4, async=True)

    @override_config(AUTO_CLOSE_SESSIONS_GRACE_PERIOD_DAYS=6)
    def test_close_outdated_with_different_config(self):
        """Test the ``close_outdated_training_sessions`` task with a custom config"""

        # The ``days_after`` arg is correctly set if we override the default config
        with patch.object(TrainingSession.objects, 'close_outdated') as patched:
            close_outdated_training_sessions()

        # The manager method was called once, with the correct args
        patched.assert_called_once_with(
            days_after=6,
            async=True
        )

    @override_config(AUTO_CLOSE_SESSIONS_ACTIVATE=False)
    def test_close_outdated_without_activate_auto_close(self):
        """Test that the ``close_outdated_training_sessions`` should not call the manager

        The constance config ``AUTO_CLOSE_SESSIONS_ACTIVATE`` is ``True`` per default.

        """

        # Call the task
        with patch('mtp.apps.trainings.tasks.close_outdated_training_sessions') as patched:
            close_outdated_training_sessions()

        # The manager method was not called
        patched.assert_not_called()

    def test_close_outdated_training_sessions_result_is_picklable(self):
        """Test the ``close_outdated_training_sessions`` task result"""

        result = close_outdated_training_sessions.delay()

        # The task result is picklable
        pickle.dumps(result, protocol=2)

    def test_close_outdated_training_sessions_arg_is_picklable(self):
        """Test the ``close_outdated_training_sessions`` task arg"""

        with patch.object(close_outdated_training_sessions, 'delay') as patched:
            close_outdated_training_sessions.delay(timedelta(days=1))

        # It will raise if args/kwargs are not picklable
        # We use protocol 2 as in celery
        # (cf http://docs.celeryproject.org/projects/kombu/en/latest/userguide/serialization.html)
        for args_list in patched.call_args_list:
            pickle.dumps(args_list[0], protocol=2)  # args
            pickle.dumps(args_list[1], protocol=2)  # kwargs


class TrainingArchiveTaskTestCase(MTPTestCase):
    """Test cases for the ``archive_training`` task"""

    def test_call_the_task_should_correctly_archive_the_provided_training(self):
        """Test that calling the task should correctly call ``archive`` on the model"""

        with patch.object(Training, 'archive') as patched:
            archive_training(self.training.pk)

        patched.assert_called_once_with()
