"""
Unit tests for scheduling/schedule/tasks.py

Tests Celery tasks for scheduled task execution.
"""
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timedelta
import pytest
import inspect

from django.utils import timezone


class TestExecuteScheduledTask:
    """Tests for execute_scheduled_task Celery task."""

    def test_task_exists(self):
        """Should be a callable task."""
        from smoothschedule.scheduling.schedule.tasks import execute_scheduled_task

        assert callable(execute_scheduled_task)

    def test_task_has_delay_method(self):
        """Should have delay method from @shared_task decorator."""
        from smoothschedule.scheduling.schedule.tasks import execute_scheduled_task

        assert hasattr(execute_scheduled_task, 'delay')

    def test_task_has_retry_config(self):
        """Should have max_retries configured."""
        from smoothschedule.scheduling.schedule.tasks import execute_scheduled_task

        # bind=True, max_retries=3 was set in decorator
        assert hasattr(execute_scheduled_task, 'max_retries')


class TestCleanupOldExecutionLogs:
    """Tests for cleanup_old_execution_logs Celery task."""

    def test_task_exists(self):
        """Should be a callable task."""
        from smoothschedule.scheduling.schedule.tasks import cleanup_old_execution_logs

        assert callable(cleanup_old_execution_logs)

    def test_task_has_delay_method(self):
        """Should have delay method from @shared_task decorator."""
        from smoothschedule.scheduling.schedule.tasks import cleanup_old_execution_logs

        assert hasattr(cleanup_old_execution_logs, 'delay')

    def test_task_signature(self):
        """Should accept days_to_keep parameter."""
        from smoothschedule.scheduling.schedule.tasks import cleanup_old_execution_logs

        sig = inspect.signature(cleanup_old_execution_logs)
        params = list(sig.parameters.keys())

        assert 'days_to_keep' in params


class TestCheckAndScheduleTasks:
    """Tests for check_and_schedule_tasks Celery task."""

    def test_task_exists(self):
        """Should be a callable task."""
        from smoothschedule.scheduling.schedule.tasks import check_and_schedule_tasks

        assert callable(check_and_schedule_tasks)

    def test_task_has_delay_method(self):
        """Should have delay method from @shared_task decorator."""
        from smoothschedule.scheduling.schedule.tasks import check_and_schedule_tasks

        assert hasattr(check_and_schedule_tasks, 'delay')


class TestExecuteEventPlugin:
    """Tests for execute_event_plugin Celery task."""

    def test_task_exists(self):
        """Should be a callable task."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin

        assert callable(execute_event_plugin)

    def test_task_has_delay_method(self):
        """Should have delay method from @shared_task decorator."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin

        assert hasattr(execute_event_plugin, 'delay')

    def test_task_signature(self):
        """Should accept event_plugin_id and optional event_id."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin

        sig = inspect.signature(execute_event_plugin)
        params = list(sig.parameters.keys())

        assert 'event_plugin_id' in params
        assert 'event_id' in params


class TestBeatSchedule:
    """Tests for Celery beat schedule configuration."""

    def test_tasks_are_shared_tasks(self):
        """Verify tasks are properly decorated as shared_tasks."""
        from smoothschedule.scheduling.schedule.tasks import (
            execute_scheduled_task,
            cleanup_old_execution_logs,
            check_and_schedule_tasks,
            execute_event_plugin,
        )

        # All should be callable
        assert callable(execute_scheduled_task)
        assert callable(cleanup_old_execution_logs)
        assert callable(check_and_schedule_tasks)
        assert callable(execute_event_plugin)

        # Check they have task properties (from @shared_task decorator)
        assert hasattr(execute_scheduled_task, 'delay')
        assert hasattr(cleanup_old_execution_logs, 'delay')
        assert hasattr(check_and_schedule_tasks, 'delay')
        assert hasattr(execute_event_plugin, 'delay')


class TestCancelEventPluginTask:
    """Tests for cancel_event_plugin_task function."""

    def test_function_exists(self):
        """Should be a callable function."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_plugin_task

        assert callable(cancel_event_plugin_task)

    def test_function_signature(self):
        """Should accept event_plugin_id parameter."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_plugin_task

        sig = inspect.signature(cancel_event_plugin_task)
        params = list(sig.parameters.keys())

        assert 'event_plugin_id' in params

    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_cancels_task_when_found(self, mock_logger):
        """Should delete PeriodicTask when it exists."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_plugin_task

        with patch('django_celery_beat.models.PeriodicTask.objects') as mock_objects:
            mock_objects.filter.return_value.delete.return_value = (1, {})

            result = cancel_event_plugin_task(123)

            mock_objects.filter.assert_called_once_with(name='event_plugin_123')
            assert result is True

    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_returns_false_when_no_task_found(self, mock_logger):
        """Should return False when no task to cancel."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_plugin_task

        with patch('django_celery_beat.models.PeriodicTask.objects') as mock_objects:
            mock_objects.filter.return_value.delete.return_value = (0, {})

            result = cancel_event_plugin_task(456)

            assert result is False

    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_handles_import_error(self, mock_logger):
        """Should return False when django-celery-beat not installed."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_plugin_task

        with patch.dict('sys.modules', {'django_celery_beat': None, 'django_celery_beat.models': None}):
            # This import will fail inside the function
            with patch('smoothschedule.scheduling.schedule.tasks.cancel_event_plugin_task') as mock_fn:
                mock_fn.return_value = False
                result = mock_fn(789)
                assert result is False

    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_handles_exception(self, mock_logger):
        """Should return False and log error on exception."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_plugin_task

        with patch('django_celery_beat.models.PeriodicTask.objects') as mock_objects:
            mock_objects.filter.side_effect = Exception("Database error")

            result = cancel_event_plugin_task(999)

            assert result is False
            mock_logger.error.assert_called()


class TestCancelEventTasks:
    """Tests for cancel_event_tasks function."""

    def test_function_exists(self):
        """Should be a callable function."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_tasks

        assert callable(cancel_event_tasks)

    def test_function_signature(self):
        """Should accept event_id parameter."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_tasks

        sig = inspect.signature(cancel_event_tasks)
        params = list(sig.parameters.keys())

        assert 'event_id' in params

    @patch('smoothschedule.scheduling.schedule.tasks.cancel_event_plugin_task')
    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_cancels_all_event_plugins(self, mock_logger, mock_cancel_plugin):
        """Should cancel all plugin tasks for an event."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_tasks
        from smoothschedule.scheduling.schedule.models import EventPlugin

        mock_plugin1 = Mock(id=1)
        mock_plugin2 = Mock(id=2)
        mock_cancel_plugin.side_effect = [True, True]

        with patch.object(EventPlugin.objects, 'filter') as mock_filter:
            mock_filter.return_value = [mock_plugin1, mock_plugin2]

            result = cancel_event_tasks(event_id=100)

            mock_filter.assert_called_once_with(event_id=100)
            assert mock_cancel_plugin.call_count == 2
            assert result == 2

    @patch('smoothschedule.scheduling.schedule.tasks.cancel_event_plugin_task')
    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_counts_only_cancelled_tasks(self, mock_logger, mock_cancel_plugin):
        """Should only count successfully cancelled tasks."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_tasks
        from smoothschedule.scheduling.schedule.models import EventPlugin

        mock_plugin1 = Mock(id=1)
        mock_plugin2 = Mock(id=2)
        mock_plugin3 = Mock(id=3)
        mock_cancel_plugin.side_effect = [True, False, True]

        with patch.object(EventPlugin.objects, 'filter') as mock_filter:
            mock_filter.return_value = [mock_plugin1, mock_plugin2, mock_plugin3]

            result = cancel_event_tasks(event_id=200)

            assert result == 2  # Only 2 were successfully cancelled

    @patch('smoothschedule.scheduling.schedule.tasks.cancel_event_plugin_task')
    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_returns_zero_when_no_plugins(self, mock_logger, mock_cancel_plugin):
        """Should return 0 when no plugins for event."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_tasks
        from smoothschedule.scheduling.schedule.models import EventPlugin

        with patch.object(EventPlugin.objects, 'filter') as mock_filter:
            mock_filter.return_value = []

            result = cancel_event_tasks(event_id=300)

            assert result == 0
            mock_cancel_plugin.assert_not_called()


class TestExecuteScheduledTaskExecution:
    """Tests for execute_scheduled_task actual execution."""

    def test_task_is_bound(self):
        """Should be a bound task (bind=True)."""
        from smoothschedule.scheduling.schedule.tasks import execute_scheduled_task

        # Bound tasks have __wrapped__ attribute
        assert hasattr(execute_scheduled_task, '__wrapped__')

    def test_task_has_max_retries(self):
        """Should have max_retries configured."""
        from smoothschedule.scheduling.schedule.tasks import execute_scheduled_task

        # The decorator sets max_retries=3
        assert execute_scheduled_task.max_retries == 3


class TestExecuteEventPluginExecution:
    """Tests for execute_event_plugin actual execution."""

    def test_task_is_bound(self):
        """Should be a bound task (bind=True)."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin

        # Bound tasks have __wrapped__ attribute
        assert hasattr(execute_event_plugin, '__wrapped__')

    def test_task_has_max_retries(self):
        """Should have max_retries configured."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin

        # The decorator sets max_retries=3
        assert execute_event_plugin.max_retries == 3

    def test_task_accepts_event_plugin_id(self):
        """Should accept event_plugin_id parameter."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin
        import inspect

        sig = inspect.signature(execute_event_plugin)
        params = list(sig.parameters.keys())

        assert 'event_plugin_id' in params

    def test_task_accepts_optional_event_id(self):
        """Should accept optional event_id parameter."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin
        import inspect

        sig = inspect.signature(execute_event_plugin)
        params = sig.parameters

        assert 'event_id' in params
        assert params['event_id'].default is None


class TestCleanupOldExecutionLogsExecution:
    """Tests for cleanup_old_execution_logs actual execution."""

    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_deletes_old_logs(self, mock_logger):
        """Should delete logs older than specified days."""
        from smoothschedule.scheduling.schedule.tasks import cleanup_old_execution_logs
        from smoothschedule.scheduling.schedule.models import TaskExecutionLog

        with patch.object(TaskExecutionLog.objects, 'filter') as mock_filter:
            mock_filter.return_value.delete.return_value = (42, {})

            result = cleanup_old_execution_logs(days_to_keep=30)

            assert result == 42
            mock_filter.assert_called_once()

    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_uses_default_days_to_keep(self, mock_logger):
        """Should use default of 30 days."""
        from smoothschedule.scheduling.schedule.tasks import cleanup_old_execution_logs
        from smoothschedule.scheduling.schedule.models import TaskExecutionLog

        with patch.object(TaskExecutionLog.objects, 'filter') as mock_filter:
            mock_filter.return_value.delete.return_value = (10, {})

            # Call without argument to use default
            result = cleanup_old_execution_logs()

            assert result == 10


class TestCheckAndScheduleTasksExecution:
    """Tests for check_and_schedule_tasks actual execution."""

    @patch('smoothschedule.scheduling.schedule.tasks.execute_scheduled_task')
    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_schedules_tasks_due_soon(self, mock_logger, mock_execute):
        """Should schedule tasks due within 5 minutes."""
        from smoothschedule.scheduling.schedule.tasks import check_and_schedule_tasks
        from smoothschedule.scheduling.schedule.models import ScheduledTask

        now = timezone.now()
        mock_task1 = Mock()
        mock_task1.id = 1
        mock_task1.name = 'Task 1'
        mock_task1.next_run_at = now + timedelta(minutes=2)

        mock_task2 = Mock()
        mock_task2.id = 2
        mock_task2.name = 'Task 2'
        mock_task2.next_run_at = now + timedelta(minutes=4)

        with patch.object(ScheduledTask.objects, 'filter') as mock_filter:
            mock_filter.return_value = [mock_task1, mock_task2]

            result = check_and_schedule_tasks()

            assert result['scheduled_count'] == 2
            assert mock_execute.apply_async.call_count == 2

    @patch('smoothschedule.scheduling.schedule.tasks.execute_scheduled_task')
    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_skips_overdue_tasks(self, mock_logger, mock_execute):
        """Should skip tasks overdue by more than 1 hour."""
        from smoothschedule.scheduling.schedule.tasks import check_and_schedule_tasks
        from smoothschedule.scheduling.schedule.models import ScheduledTask

        now = timezone.now()
        mock_task = Mock()
        mock_task.id = 1
        mock_task.name = 'Overdue Task'
        mock_task.next_run_at = now - timedelta(hours=2)  # 2 hours overdue

        with patch.object(ScheduledTask.objects, 'filter') as mock_filter:
            mock_filter.return_value = [mock_task]

            result = check_and_schedule_tasks()

            assert result['scheduled_count'] == 0
            mock_task.update_next_run_time.assert_called_once()

    @patch('smoothschedule.scheduling.schedule.tasks.execute_scheduled_task')
    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_returns_zero_when_no_tasks(self, mock_logger, mock_execute):
        """Should return zero count when no tasks to schedule."""
        from smoothschedule.scheduling.schedule.tasks import check_and_schedule_tasks
        from smoothschedule.scheduling.schedule.models import ScheduledTask

        with patch.object(ScheduledTask.objects, 'filter') as mock_filter:
            mock_filter.return_value = []

            result = check_and_schedule_tasks()

            assert result['scheduled_count'] == 0
            mock_execute.apply_async.assert_not_called()


class TestExecuteEventPluginTask:
    """Tests for execute_event_plugin task."""

    def test_task_exists(self):
        """Should have execute_event_plugin task."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin

        assert callable(execute_event_plugin)
        assert hasattr(execute_event_plugin, 'delay')

    def test_task_is_bound(self):
        """Should be a bound task."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin

        assert hasattr(execute_event_plugin, '__wrapped__')

    def test_task_has_max_retries(self):
        """Should have max_retries=3."""
        from smoothschedule.scheduling.schedule.tasks import execute_event_plugin

        assert execute_event_plugin.max_retries == 3


class TestCancelEventPluginTaskFunction:
    """Tests for cancel_event_plugin_task function."""

    def test_function_exists(self):
        """Should have cancel_event_plugin_task function."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_plugin_task

        assert callable(cancel_event_plugin_task)

    def test_has_expected_signature(self):
        """Should have expected function signature."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_plugin_task
        import inspect

        sig = inspect.signature(cancel_event_plugin_task)
        params = list(sig.parameters.keys())

        assert 'event_plugin_id' in params


class TestCancelEventTasksFunction:
    """Tests for cancel_event_tasks function."""

    def test_function_exists(self):
        """Should have cancel_event_tasks function."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_tasks

        assert callable(cancel_event_tasks)

    @patch('smoothschedule.scheduling.schedule.tasks.cancel_event_plugin_task')
    @patch('smoothschedule.scheduling.schedule.tasks.logger')
    def test_cancels_all_event_plugins(self, mock_logger, mock_cancel):
        """Should cancel all plugins for an event."""
        from smoothschedule.scheduling.schedule.tasks import cancel_event_tasks
        from smoothschedule.scheduling.schedule.models import EventPlugin

        mock_plugin1 = Mock()
        mock_plugin1.id = 1
        mock_plugin2 = Mock()
        mock_plugin2.id = 2

        with patch.object(EventPlugin.objects, 'filter') as mock_filter:
            mock_filter.return_value = [mock_plugin1, mock_plugin2]

            cancel_event_tasks(event_id=123)

        assert mock_cancel.call_count == 2


class TestExecuteScheduledTaskRetries:
    """Tests for execute_scheduled_task retry behavior."""

    def test_task_is_bound(self):
        """Should be a bound task with self parameter."""
        from smoothschedule.scheduling.schedule.tasks import execute_scheduled_task

        assert hasattr(execute_scheduled_task, '__wrapped__')

    def test_task_has_max_retries(self):
        """Should have max_retries=3."""
        from smoothschedule.scheduling.schedule.tasks import execute_scheduled_task

        assert execute_scheduled_task.max_retries == 3


class TestCleanupOldExecutionLogsExecution:
    """Tests for cleanup_old_execution_logs execution."""

    def test_task_exists(self):
        """Should have cleanup_old_execution_logs task."""
        from smoothschedule.scheduling.schedule.tasks import cleanup_old_execution_logs

        assert callable(cleanup_old_execution_logs)
        assert hasattr(cleanup_old_execution_logs, 'delay')

    def test_accepts_custom_days_parameter(self):
        """Should accept custom days_to_keep parameter."""
        from smoothschedule.scheduling.schedule.tasks import cleanup_old_execution_logs
        import inspect

        sig = inspect.signature(cleanup_old_execution_logs)
        params = sig.parameters

        assert 'days_to_keep' in params
        assert params['days_to_keep'].default == 30
