"""This module holds the tests for the celery tasks of the ``tasks`` app"""

from uuid import uuid4

from celery import states
from mock import patch

from mtp.celery import app
from mtp.apps.common.tests.test_cases import MTPTestCase

from ..models import DjangoTask
from ..tasks import DjangoBackedCeleryTask


@app.task(bind=True, base=DjangoBackedCeleryTask)
def log_something(self):
    """Simple task that log something in the tied django task instance"""

    django_task = self.get_django_task()
    django_task.logger.info('The task is executing...')

    return 'Hello %s' % django_task.user.username


class DjangoBackedCeleryTaskTestCase(MTPTestCase):
    """Test case for the ``DjangoBackedCeleryTask`` celery abstract task"""

    def test_call_task_without_user_should_raise_an_exception(self):
        """The task should failed if the user argument is not set"""

        async_result = log_something.delay()
        self.assertEqual(async_result.state, 'FAILURE')
        self.assertIsInstance(async_result.result, TypeError)

    def test_call_task_without_a_correct_user_instance_should_raise_an_exception(self):
        """The task should failed if the user argument is not a valid one"""

        async_result = log_something.delay('user')
        self.assertEqual(async_result.state, 'FAILURE')
        self.assertIsInstance(async_result.result, ValueError)

    def test_call_task_with_a_valid_user_should_work(self):
        """The task should work when called with a valid user instance"""

        admin_async_result = log_something.delay(self.admin)

        # We should have a new ``DjangoTask`` instance for the admin user
        admin_django_task = DjangoTask.objects.first()

        # The user is correct
        self.assertEqual(
            admin_django_task.user,
            self.admin
        )

        # The task id is correct
        self.assertEqual(
            str(admin_django_task.celery_task_id),
            admin_async_result.id
        )

        # The django task instance can be retrieved
        self.assertEqual(
            log_something.get_django_task(admin_async_result.id),
            admin_django_task
        )

        # The ``finished_at`` attribute is not empty
        self.assertIsNotNone(admin_django_task.finished_at)
        # And the state is `PENDING`

        # We should also have a log entry
        self.assertEqual(
            admin_django_task.logger.last_entry()['msg'],
            'The task is executing...'
        )

        # And a correct ``AsyncResult``...
        self.assertIsNotNone(admin_django_task.celery_async_result)

        # Start another task for another user
        trainer_async_result = log_something.delay(self.trainer)

        # We should have a new ``DjangoTask`` instance for the trainer user
        trainer_django_task = DjangoTask.objects.last()

        # The user is correct
        self.assertEqual(
            trainer_django_task.user,
            self.trainer
        )

        # The task id is correct
        self.assertEqual(
            str(trainer_django_task.celery_task_id),
            trainer_async_result.id
        )

        # The ``finished_at`` attribute is not empty
        self.assertIsNotNone(trainer_django_task.finished_at)

        # We should also have a log entry
        self.assertEqual(
            trainer_django_task.logger.last_entry()['msg'],
            'The task is executing...'
        )

    def test_call_task_with_user_set_to_none_should_work(self):
        """The task should work if the user argument is provided as ``None``"""

        # Create a task with no user
        task_result = log_something.delay(user=None)

        # We should have a new ``DjangoTask`` instance
        django_task = DjangoTask.objects.last()

        # Ensure the task id is correct
        self.assertEqual(
            str(django_task.celery_task_id),
            task_result.id
        )

        # And there is not user tied to this task
        self.assertIsNone(django_task.user_id)


class DjangoTaskStateTestCase(MTPTestCase):
    """Test the different states of the ``DjangoTask`` model"""

    @classmethod
    def setUpTestData(cls):
        """Create an instance of ``DjangoTask`` and a fake celery task"""

        super().setUpTestData()

        cls.django_task = DjangoTask.objects.create(
            celery_task_id=uuid4(),
            user=cls.admin,
        )

        cls.task = DjangoBackedCeleryTask()
        cls.task.django_task = cls.django_task

    def test_default_state(self):
        """Test that the default state is ``PENDING``"""

        self.django_task.refresh_from_db()
        self.assertEqual(self.django_task.state, self.django_task.TASK_STATES.PENDING)

    def test_state_on_task_failure(self):
        """Test that the state is set to ``FAILURE`` when the task fails"""

        with patch.object(DjangoTask, 'manage_result') as patched:
            self.task.on_failure(task_id=self.django_task.celery_task_id,
                                 exc=None, args=(), kwargs={}, einfo=None)

        self.django_task.refresh_from_db()
        self.assertEqual(self.django_task.state, self.django_task.TASK_STATES.FAILURE)

        # The ``manage_result`` method of the task should not have been called
        patched.assert_not_called()

    def test_state_on_task_success(self):
        """Test that the state is set to ``SUCCESS`` when the task is a success"""

        with patch.object(DjangoTask, 'manage_result') as patched:
            self.task.on_success(task_id=self.django_task.celery_task_id,
                                 retval=123, args=(), kwargs={})

        self.django_task.refresh_from_db()
        self.assertEqual(self.django_task.state, self.django_task.TASK_STATES.SUCCESS)

        # The ``manage_result`` method of the task should have been called
        patched.assert_called_once_with(123)

    def test_state_on_task_retry(self):
        """Test that the state is set to ``RETRY`` when the task is retried"""

        with patch.object(DjangoTask, 'manage_result') as patched:
            self.task.on_retry(task_id=self.django_task.celery_task_id,
                               exc=None, args=(), kwargs={}, einfo=None)

        self.django_task.refresh_from_db()
        self.assertEqual(self.django_task.state, self.django_task.TASK_STATES.RETRY)

        # The ``manage_result`` method of the task should not have been called
        patched.assert_not_called()

    def test_state_after_return(self):
        """Test that the state is set to  the given state when after the task returns"""

        with patch.object(DjangoTask, 'manage_result') as patched:
            self.task.after_return(status=states.SUCCESS, task_id=self.django_task.celery_task_id,
                                   retval=123, args=(), kwargs={}, einfo=None)

        self.django_task.refresh_from_db()
        self.assertEqual(self.django_task.state, self.django_task.TASK_STATES.SUCCESS)

        # The ``manage_result`` method of the task should not have been called
        patched.assert_not_called()

        with patch.object(DjangoTask, 'manage_result') as patched:
            self.task.after_return(status=states.FAILURE, task_id=self.django_task.celery_task_id,
                                   retval=None, args=(), kwargs={}, einfo=None)
        self.django_task.refresh_from_db()
        self.assertEqual(self.django_task.state, self.django_task.TASK_STATES.FAILURE)

        # The ``manage_result`` method of the task should not have been called
        patched.assert_not_called()
