import mock
from datetime import timedelta
from decimal import Decimal

from django.db.models import Count
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone

from ocyan.task.models import Task
from ocyan.task.choices import TaskStatus
from example.actions import TestAction

from ocyan.task.choices import LogType
from ocyan.task.cron import cleanup_old_tasks


class TaskTest(TestCase):
    def setUp(self):
        self.maxDiff = None
        super().setUp()

    def test_name(self):
        task = Task.create(TestAction())
        self.assertEqual(
            task.name,
            "Test action test name",
            "task name should be based on action name class attribute",
        )

        task = Task.create(TestAction(name="I am henk"))
        self.assertEqual(
            task.name,
            "Test action I am henk",
            "task name should be based on argument passed to action constructor",
        )

    def test_code(self):
        task = Task.create(TestAction())
        self.assertEqual(
            task.code,
            "test code",
            "task code should be the same as the class variable code",
        )

        task = Task.create(TestAction(code="I am henk"))
        self.assertEqual(
            task.code,
            "I am henk",
            "task code should be the same as the code argument passed to the task constructor",
        )

    def test_data(self):
        data = {
            "lol": 1.6,
            "henk": "henk",
            "braadwridt": False,
            "soep": {"lkker": True, "klats": None},
            "hoela": [1, True, "henk", {}, None, 6.0],
        }
        task = Task.create(TestAction(data))
        Task.objects.get()

        self.assertDictEqual(task.data, data)

    def test_data_date_not_supported(self):
        "date or datetime type are not supported in data dicts for actions"
        with self.assertRaises(
            ValidationError, msg="date type is not supported in data"
        ) as error:
            Task.create(TestAction({"date": timezone.now()}))

        self.assertEqual(
            str(error.exception), "{'data': ['Waarde moet valide JSON zijn.']}"
        )

    def test_data_decimal_not_supported(self):
        "You can't store decimals in a data dict of an action"
        with self.assertRaises(ValidationError) as error:
            Task.create(TestAction({"price": Decimal(4)}))

        self.assertEqual(
            str(error.exception), "{'data': ['Waarde moet valide JSON zijn.']}"
        )

    def test_get_retry_after(self):
        "Actions will be automatically retried Task.retry_after time after the last time the action was saved"
        action = TestAction()
        task = Task.create(action)
        self.assertIsNone(
            task.retry_datetime(), "Task will only get retries when they are failed"
        )
        task.status = TaskStatus.FAILED
        task.save()
        date_updated = task.date_updated
        self.assertEqual(task.retry_datetime(), date_updated + action.get_retry_after())

    def test_get_importstring(self):
        importstring = TestAction.get_importstring()
        self.assertIn(TestAction.__name__, importstring)
        self.assertTrue(importstring.startswith(TestAction.__module__))

    def test_initialize_task(self):
        "Task objects are save to the database"
        self.assertFalse(Task.objects.exists())

        Task.create(
            TestAction(name="Test Task"),
        )

        self.assertEqual(Task.objects.count(), 1)

    def test_task_run_normaly(self):
        "Task can be executed and the result and some logging is stored in the database"
        self.assertFalse(Task.objects.exists())
        task = Task.create(
            TestAction(name="Test Task"),
        )

        self.assertEqual(task.status, TaskStatus.PENDING)
        self.assertEqual(task.logs.count(), 1)

        task.run()

        task.refresh_from_db()

        self.assertEqual(task.status, TaskStatus.SUCCESSFUL)
        self.assertEqual(task.logs.count(), 6)

        log_type_count = (
            task.logs.values("log_type")
            .order_by("log_type")
            .annotate(Count("log_type"))
        )
        self.assertSequenceEqual(
            log_type_count,
            [
                {"log_type": LogType.DEBUG, "log_type__count": 1},
                {"log_type": LogType.PROGRESS, "log_type__count": 1},
                {"log_type": LogType.SUCCESSFUL, "log_type__count": 1},
                {"log_type": LogType.INFO, "log_type__count": 3},
            ],
        )

    @mock.patch("ocyan.task.models.logger")
    def test_task_run_failed(self, mock_logger):
        "Task should be able to fail and then retried again."
        self.assertFalse(Task.objects.exists())
        task = Task.create(
            TestAction({"raise_run": True}, name="Test Task"),
        )

        self.assertEqual(task.status, TaskStatus.PENDING)
        self.assertEqual(task.logs.count(), 1)

        task.run()

        # Task.run catches all exceptions and logs them.
        mock_logger.exception.assert_called_once()

        task.refresh_from_db()
        self.assertEqual(task.status, TaskStatus.FAILED)
        self.assertEqual(task.logs.count(), 6)

        log_type_count = (
            task.logs.values("log_type")
            .order_by("log_type")
            .annotate(Count("log_type"))
        )
        self.assertSequenceEqual(
            log_type_count,
            [
                {"log_type": LogType.DEBUG, "log_type__count": 1},
                {"log_type": LogType.PROGRESS, "log_type__count": 1},
                {"log_type": LogType.FAILED, "log_type__count": 1},
                {"log_type": LogType.INFO, "log_type__count": 2},
                {"log_type": LogType.ERROR, "log_type__count": 1},
            ],
        )

        # reset action
        task.data = None
        task._action = None  # pylint:disable=W0212

        task.save()
        task.run()

        task.refresh_from_db()

        self.assertEqual(task.status, TaskStatus.SUCCESSFUL)
        self.assertEqual(task.logs.count(), 11)

        log_type_count = (
            task.logs.values("log_type")
            .order_by("log_type")
            .annotate(Count("log_type"))
        )
        self.assertSequenceEqual(
            log_type_count,
            [
                {"log_type": LogType.DEBUG, "log_type__count": 2},
                {"log_type": LogType.PROGRESS, "log_type__count": 2},
                {"log_type": LogType.SUCCESSFUL, "log_type__count": 1},
                {"log_type": LogType.FAILED, "log_type__count": 1},
                {"log_type": LogType.INFO, "log_type__count": 4},
                {"log_type": LogType.ERROR, "log_type__count": 1},
            ],
        )

    def test_old_task(self):
        Task.create(
            TestAction({"raise_run": True}, name="Test Task"),
        )
        Task.objects.update(date_updated=timezone.now() - timedelta(weeks=10))
        self.assertEqual(Task.objects.count(), 1)

        cleanup_old_tasks("ARGH")
        self.assertEqual(Task.objects.count(), 0)
