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

Tests signal handlers for event plugins, status changes, and notifications.
"""

from datetime import datetime, timedelta
from unittest.mock import Mock, patch, MagicMock

import pytest
from django.utils import timezone

from smoothschedule.scheduling.schedule.signals import (
    emit_status_change,
    event_status_changed,
    customer_notification_requested,
    time_off_request_submitted,
    TIME_BLOCK_APPROVAL_FIELDS,
)


class TestEmitStatusChange:
    """Tests for emit_status_change helper function."""

    def test_sends_signal_with_correct_args(self):
        """Should send signal with all required arguments."""
        received_kwargs = {}

        def receiver(sender, **kwargs):
            received_kwargs.update(kwargs)

        event_status_changed.connect(receiver)

        try:
            event = Mock()
            event.__class__ = Mock
            changed_by = Mock()
            tenant = Mock()

            emit_status_change(
                event=event,
                old_status='SCHEDULED',
                new_status='IN_PROGRESS',
                changed_by=changed_by,
                tenant=tenant,
                skip_notifications=True,
            )

            assert received_kwargs['event'] == event
            assert received_kwargs['old_status'] == 'SCHEDULED'
            assert received_kwargs['new_status'] == 'IN_PROGRESS'
            assert received_kwargs['changed_by'] == changed_by
            assert received_kwargs['tenant'] == tenant
            assert received_kwargs['skip_notifications'] is True

        finally:
            event_status_changed.disconnect(receiver)

    def test_sends_signal_with_default_skip_notifications(self):
        """Should send signal with skip_notifications defaulting to False."""
        received_kwargs = {}

        def receiver(sender, **kwargs):
            received_kwargs.update(kwargs)

        event_status_changed.connect(receiver)

        try:
            event = Mock()
            event.__class__ = Mock

            emit_status_change(
                event=event,
                old_status='SCHEDULED',
                new_status='COMPLETED',
                changed_by=Mock(),
                tenant=Mock(),
            )

            assert received_kwargs['skip_notifications'] is False

        finally:
            event_status_changed.disconnect(receiver)


class TestSignalDefinitions:
    """Tests for signal definitions."""

    def test_event_status_changed_signal_exists(self):
        """Should have event_status_changed signal defined."""
        assert event_status_changed is not None

    def test_customer_notification_requested_signal_exists(self):
        """Should have customer_notification_requested signal defined."""
        assert customer_notification_requested is not None

    def test_time_off_request_submitted_signal_exists(self):
        """Should have time_off_request_submitted signal defined."""
        assert time_off_request_submitted is not None


class TestTimeBlockApprovalFields:
    """Tests for TIME_BLOCK_APPROVAL_FIELDS constant."""

    def test_contains_required_fields(self):
        """Should contain all fields that trigger re-approval."""
        required_fields = [
            'title', 'start_date', 'end_date', 'all_day',
            'start_time', 'end_time', 'recurrence_type',
        ]

        for field in required_fields:
            assert field in TIME_BLOCK_APPROVAL_FIELDS

    def test_is_list(self):
        """Should be a list type."""
        assert isinstance(TIME_BLOCK_APPROVAL_FIELDS, list)


class TestRescheduleEventCeleryTasksLogic:
    """Tests for reschedule logic without heavy patching."""

    def test_time_based_triggers_list(self):
        """Verify time-based triggers are correctly identified."""
        from smoothschedule.scheduling.schedule.signals import reschedule_event_celery_tasks

        # The function uses these triggers internally
        time_based_triggers = ['before_start', 'at_start', 'after_start', 'after_end']

        # Test that at_start affects start
        assert 'at_start' in time_based_triggers

        # Test that after_end affects end
        assert 'after_end' in time_based_triggers


class TestBroadcastEventChangeSyncWithRealChannels:
    """Tests for broadcast_event_change_sync with channel layer mocking."""

    @patch('channels.layers.get_channel_layer')
    def test_does_nothing_without_channel_layer(self, mock_get_layer):
        """Should handle missing channel layer gracefully."""
        from smoothschedule.scheduling.schedule.signals import broadcast_event_change_sync

        mock_get_layer.return_value = None
        event = Mock(id=123)

        # Should not raise
        broadcast_event_change_sync(event, 'event_created')


class TestTrackEventChangesWithModel:
    """Tests for track_event_changes with model patching."""

    def test_sets_none_for_new_event(self):
        """Should set None for old times on new events."""
        from smoothschedule.scheduling.schedule.signals import track_event_changes

        instance = Mock()
        instance.pk = None

        track_event_changes(Mock(), instance)

        assert instance._old_start_time is None
        assert instance._old_end_time is None


class TestTrackEventStatusChangeWithModel:
    """Tests for track_event_status_change with model patching."""

    def test_sets_none_for_new_event(self):
        """Should set None for new events."""
        from smoothschedule.scheduling.schedule.signals import track_event_status_change

        instance = Mock()
        instance.pk = None

        track_event_status_change(Mock(), instance)

        assert instance._old_status is None


class TestTrackEventPluginActiveChange:
    """Tests for track_event_plugin_active_change signal handler."""

    def test_sets_none_for_new_plugin(self):
        """Should set None for new plugins."""
        from smoothschedule.scheduling.schedule.signals import track_event_plugin_active_change

        instance = Mock()
        instance.pk = None

        track_event_plugin_active_change(Mock(), instance)

        assert instance._was_active is None


class TestTrackTimeBlockChangesNew:
    """Tests for track_time_block_changes with new blocks."""

    def test_sets_defaults_for_new_block(self):
        """Should set default values for new time blocks."""
        from smoothschedule.scheduling.schedule.signals import track_time_block_changes

        instance = Mock()
        instance.pk = None

        track_time_block_changes(Mock(), instance)

        assert instance._old_approval_status is None
        assert instance._was_approved is False
        assert instance._changed_fields == []
        assert instance._needs_re_approval_notification is False


class TestRescheduleEventPluginsOnChangeLogic:
    """Tests for reschedule_event_plugins_on_change logic."""

    def test_does_nothing_on_create(self):
        """Should not reschedule on new event creation."""
        from smoothschedule.scheduling.schedule.signals import reschedule_event_plugins_on_change

        instance = Mock()

        with patch('smoothschedule.scheduling.schedule.signals.reschedule_event_celery_tasks') as mock_reschedule:
            reschedule_event_plugins_on_change(Mock(), instance, created=True)

            mock_reschedule.assert_not_called()

    def test_does_nothing_without_old_times(self):
        """Should not reschedule when old times not tracked."""
        from smoothschedule.scheduling.schedule.signals import reschedule_event_plugins_on_change

        instance = Mock(spec=['start_time', 'end_time'])  # No _old_start_time

        with patch('smoothschedule.scheduling.schedule.signals.reschedule_event_celery_tasks') as mock_reschedule:
            reschedule_event_plugins_on_change(Mock(), instance, created=False)

            mock_reschedule.assert_not_called()


class TestAutoAttachGlobalPluginsLogic:
    """Tests for auto_attach_global_plugins logic."""

    def test_does_not_run_on_update(self):
        """Should not attach plugins when event is updated."""
        from smoothschedule.scheduling.schedule.signals import auto_attach_global_plugins

        instance = Mock()

        # The signal should return early for updates
        with patch('smoothschedule.scheduling.schedule.models.GlobalEventPlugin') as mock_gep:
            auto_attach_global_plugins(Mock(), instance, created=False)

            # Filter should not be called for updates
            mock_gep.objects.filter.assert_not_called()


class TestApplyGlobalPluginToExistingEvents:
    """Tests for apply_global_plugin_to_existing_events signal handler."""

    def test_does_nothing_on_update(self):
        """Should not apply on update."""
        from smoothschedule.scheduling.schedule.signals import apply_global_plugin_to_existing_events

        instance = Mock()
        instance.apply_to_all_events = Mock()

        apply_global_plugin_to_existing_events(Mock(), instance, created=False)

        instance.apply_to_all_events.assert_not_called()

    def test_does_nothing_when_inactive(self):
        """Should not apply when rule is inactive."""
        from smoothschedule.scheduling.schedule.signals import apply_global_plugin_to_existing_events

        instance = Mock()
        instance.is_active = False
        instance.apply_to_all_events = Mock()

        apply_global_plugin_to_existing_events(Mock(), instance, created=True)

        instance.apply_to_all_events.assert_not_called()

    def test_does_nothing_when_apply_to_existing_false(self):
        """Should not apply when apply_to_existing is False."""
        from smoothschedule.scheduling.schedule.signals import apply_global_plugin_to_existing_events

        instance = Mock()
        instance.is_active = True
        instance.apply_to_existing = False
        instance.apply_to_all_events = Mock()

        apply_global_plugin_to_existing_events(Mock(), instance, created=True)

        instance.apply_to_all_events.assert_not_called()


class TestScheduleEventPluginOnCreate:
    """Tests for schedule_event_plugin_on_create signal handler."""

    def test_skips_non_time_based_triggers(self):
        """Should skip plugins with non-time-based triggers."""
        from smoothschedule.scheduling.schedule.signals import schedule_event_plugin_on_create

        instance = Mock()
        instance.trigger = 'on_complete'

        with patch('smoothschedule.scheduling.schedule.signals.schedule_event_plugin_task') as mock_schedule:
            schedule_event_plugin_on_create(Mock(), instance, created=True)

            mock_schedule.assert_not_called()


class TestCancelEventTasksOnCancel:
    """Tests for cancel_event_tasks_on_cancel signal handler."""

    def test_does_not_cancel_on_create(self):
        """Should not cancel on new event creation."""
        from smoothschedule.scheduling.schedule.signals import cancel_event_tasks_on_cancel

        instance = Mock()

        with patch('smoothschedule.scheduling.schedule.tasks.cancel_event_tasks') as mock_cancel:
            cancel_event_tasks_on_cancel(Mock(), instance, created=True)

            # On create, cancel_event_tasks should not be called
            # But this is hard to test without the local import
            # Just verify it doesn't raise
            assert True


class TestBroadcastEventSave:
    """Tests for broadcast_event_save signal handler."""

    @patch('smoothschedule.scheduling.schedule.signals.broadcast_event_change_sync')
    @patch('smoothschedule.scheduling.schedule.signals.logger')
    def test_broadcasts_event_created(self, mock_logger, mock_broadcast):
        """Should broadcast event_created for new events."""
        from smoothschedule.scheduling.schedule.signals import broadcast_event_save

        instance = Mock(id=123)

        broadcast_event_save(Mock(), instance, created=True)

        mock_broadcast.assert_called_once_with(instance, 'event_created')

    @patch('smoothschedule.scheduling.schedule.signals.broadcast_event_change_sync')
    @patch('smoothschedule.scheduling.schedule.signals.logger')
    def test_broadcasts_status_changed(self, mock_logger, mock_broadcast):
        """Should broadcast event_status_changed when status changes."""
        from smoothschedule.scheduling.schedule.signals import broadcast_event_save

        instance = Mock(id=123)
        instance._old_status = 'SCHEDULED'
        instance.status = 'IN_PROGRESS'

        broadcast_event_save(Mock(), instance, created=False)

        mock_broadcast.assert_called_once()
        assert mock_broadcast.call_args[0][1] == 'event_status_changed'


class TestBroadcastEventDelete:
    """Tests for broadcast_event_delete signal handler."""

    @patch('smoothschedule.scheduling.schedule.signals.broadcast_event_change_sync')
    @patch('smoothschedule.scheduling.schedule.signals.logger')
    def test_broadcasts_event_deleted(self, mock_logger, mock_broadcast):
        """Should broadcast event_deleted on delete."""
        from smoothschedule.scheduling.schedule.signals import broadcast_event_delete

        instance = Mock(id=123)

        broadcast_event_delete(Mock(), instance)

        mock_broadcast.assert_called_once_with(instance, 'event_deleted')


class TestIsNotificationsAvailable:
    """Tests for is_notifications_available helper."""

    def test_returns_false_on_import_error(self):
        """Should return False when notifications app unavailable."""
        from smoothschedule.scheduling.schedule.signals import is_notifications_available

        # The function should handle import errors gracefully
        # It either returns True or False, never raises
        result = is_notifications_available()
        assert isinstance(result, bool)


class TestCreateNotificationSafe:
    """Tests for create_notification_safe helper."""

    @patch('smoothschedule.scheduling.schedule.signals.is_notifications_available')
    def test_returns_none_when_unavailable(self, mock_available):
        """Should return None when notifications not available."""
        from smoothschedule.scheduling.schedule.signals import create_notification_safe

        mock_available.return_value = False

        result = create_notification_safe(Mock(), Mock(), 'test')

        assert result is None


class TestNotifyManagersOnPendingTimeOffLogic:
    """Tests for notify_managers_on_pending_time_off logic."""

    def test_does_not_notify_for_non_pending_approved(self):
        """Should not notify when not new pending and not needing re-approval."""
        from smoothschedule.scheduling.schedule.signals import notify_managers_on_pending_time_off
        from smoothschedule.scheduling.schedule.models import TimeBlock

        instance = Mock()
        instance.approval_status = TimeBlock.ApprovalStatus.APPROVED
        instance._needs_re_approval_notification = False

        with patch('smoothschedule.scheduling.schedule.signals.create_notification_safe') as mock_notif:
            notify_managers_on_pending_time_off(Mock(), instance, created=False)

            mock_notif.assert_not_called()

    def test_does_not_notify_for_business_level_blocks(self):
        """Should not notify for business-level blocks."""
        from smoothschedule.scheduling.schedule.signals import notify_managers_on_pending_time_off
        from smoothschedule.scheduling.schedule.models import TimeBlock

        instance = Mock()
        instance.approval_status = TimeBlock.ApprovalStatus.PENDING
        instance.resource = None  # Business-level block

        with patch('smoothschedule.scheduling.schedule.signals.create_notification_safe') as mock_notif:
            notify_managers_on_pending_time_off(Mock(), instance, created=True)

            mock_notif.assert_not_called()
