"""
Unit tests for ticket signals.

Tests signal handlers, helper functions, and notification logic without database access.
Following the testing pyramid: fast, isolated unit tests using mocks.
"""
import logging
from unittest.mock import Mock, patch, MagicMock, call
from django.db.models.signals import post_save, pre_save
from django.test import override_settings

import pytest

from smoothschedule.commerce.tickets.models import Ticket, TicketComment
from smoothschedule.commerce.tickets import signals
from smoothschedule.identity.users.models import User


class TestIsNotificationsAvailable:
    """Test the is_notifications_available() helper function."""

    def test_returns_cached_true_value(self):
        """Notification availability check should return cached True value."""
        # Set the cache to True
        signals._notifications_available = True

        result = signals.is_notifications_available()

        assert result is True

    def test_returns_cached_false_value(self):
        """Notification availability check should return cached False value."""
        signals._notifications_available = False

        result = signals.is_notifications_available()

        assert result is False

    def test_function_is_callable(self):
        """Should be a callable function."""
        assert callable(signals.is_notifications_available)


class TestSendWebsocketNotification:
    """Test the send_websocket_notification() helper function."""

    def test_sends_notification_successfully(self):
        """Should send websocket notification via channel layer."""
        mock_channel_layer = MagicMock()

        with patch('smoothschedule.commerce.tickets.signals.get_channel_layer', return_value=mock_channel_layer):
            with patch('smoothschedule.commerce.tickets.signals.async_to_sync') as mock_async:
                signals.send_websocket_notification(
                    "user_123",
                    {"type": "test", "message": "Hello"}
                )

                mock_async.assert_called_once()
                # Verify the correct arguments were passed
                call_args = mock_async.call_args[0]
                assert call_args[0] == mock_channel_layer.group_send

    def test_handles_missing_channel_layer(self, caplog):
        """Should log warning when channel layer is not configured."""
        with patch('smoothschedule.commerce.tickets.signals.get_channel_layer', return_value=None):
            with caplog.at_level(logging.WARNING):
                signals.send_websocket_notification("user_123", {"type": "test"})

                assert "Channel layer not configured" in caplog.text

    def test_handles_exception_gracefully(self, caplog):
        """Should log error and not raise when websocket send fails."""
        mock_channel_layer = MagicMock()

        with patch('smoothschedule.commerce.tickets.signals.get_channel_layer', return_value=mock_channel_layer):
            with patch('smoothschedule.commerce.tickets.signals.async_to_sync', side_effect=Exception("Connection error")):
                with caplog.at_level(logging.ERROR):
                    # Should not raise
                    signals.send_websocket_notification("user_123", {"type": "test"})

                    assert "Failed to send WebSocket notification" in caplog.text
                    assert "user_123" in caplog.text


class TestCreateNotification:
    """Test the create_notification() helper function."""

    def test_skips_when_notifications_unavailable(self, caplog):
        """Should skip notification creation when app is unavailable."""
        with patch('smoothschedule.commerce.tickets.signals.is_notifications_available', return_value=False):
            with caplog.at_level(logging.DEBUG):
                signals.create_notification(
                    recipient=Mock(),
                    actor=Mock(),
                    verb="test",
                    action_object=Mock(),
                    target=Mock(),
                    data={}
                )

                assert "notifications app not available" in caplog.text


class TestGetPlatformSupportTeam:
    """Test the get_platform_support_team() helper function."""

    def test_returns_platform_team_members(self):
        """Should return users with platform roles."""
        mock_queryset = Mock()
        mock_filtered = Mock()
        mock_queryset.filter.return_value = mock_filtered

        with patch('smoothschedule.commerce.tickets.signals.User.objects', mock_queryset):
            result = signals.get_platform_support_team()

            # Verify correct filter was applied
            mock_queryset.filter.assert_called_once_with(
                role__in=[User.Role.PLATFORM_SUPPORT, User.Role.PLATFORM_MANAGER, User.Role.SUPERUSER],
                is_active=True
            )
            assert result == mock_filtered

    def test_handles_exception_gracefully(self, caplog):
        """Should return empty queryset and log error on exception."""
        mock_queryset = Mock()
        mock_queryset.filter.side_effect = Exception("DB error")
        mock_queryset.none.return_value = Mock()

        with patch('smoothschedule.commerce.tickets.signals.User.objects', mock_queryset):
            with caplog.at_level(logging.ERROR):
                result = signals.get_platform_support_team()

                assert "Failed to fetch platform support team" in caplog.text
                mock_queryset.none.assert_called_once()


class TestGetTenantManagers:
    """Test the get_tenant_managers() helper function."""

    def test_returns_tenant_managers(self):
        """Should return owners and managers for a tenant."""
        mock_tenant = Mock(id=1)
        mock_queryset = Mock()
        mock_filtered = Mock()
        mock_queryset.filter.return_value = mock_filtered

        with patch('smoothschedule.commerce.tickets.signals.User.objects', mock_queryset):
            result = signals.get_tenant_managers(mock_tenant)

            mock_queryset.filter.assert_called_once_with(
                tenant=mock_tenant,
                role__in=[User.Role.TENANT_OWNER, User.Role.TENANT_MANAGER],
                is_active=True
            )
            assert result == mock_filtered

    def test_returns_empty_queryset_when_no_tenant(self):
        """Should return empty queryset when tenant is None."""
        mock_queryset = Mock()
        mock_queryset.none.return_value = Mock()

        with patch('smoothschedule.commerce.tickets.signals.User.objects', mock_queryset):
            result = signals.get_tenant_managers(None)

            mock_queryset.none.assert_called_once()

    def test_handles_exception_gracefully(self, caplog):
        """Should return empty queryset and log error on exception."""
        mock_tenant = Mock(id=1)
        mock_queryset = Mock()
        mock_queryset.filter.side_effect = Exception("DB error")
        mock_queryset.none.return_value = Mock()

        with patch('smoothschedule.commerce.tickets.signals.User.objects', mock_queryset):
            with caplog.at_level(logging.ERROR):
                result = signals.get_tenant_managers(mock_tenant)

                assert "Failed to fetch tenant managers" in caplog.text
                mock_queryset.none.assert_called_once()


class TestTicketPreSaveHandler:
    """Test the ticket_pre_save_handler signal receiver."""

    def test_ignores_new_tickets(self):
        """Should not store state for new tickets (no pk)."""
        mock_ticket = Mock(pk=None)

        signals._ticket_pre_save_state.clear()

        signals.ticket_pre_save_handler(sender=Ticket, instance=mock_ticket)

        assert len(signals._ticket_pre_save_state) == 0

    def test_handles_does_not_exist_gracefully(self):
        """Should handle DoesNotExist exception gracefully."""
        mock_ticket = Mock(pk=999)

        signals._ticket_pre_save_state.clear()

        with patch.object(Ticket.objects, 'get', side_effect=Ticket.DoesNotExist):
            # Should not raise
            signals.ticket_pre_save_handler(sender=Ticket, instance=mock_ticket)

            assert 999 not in signals._ticket_pre_save_state


class TestTicketNotificationHandler:
    """Test the ticket_notification_handler signal receiver."""

    def test_calls_handle_ticket_creation_for_new_tickets(self):
        """Should delegate to _handle_ticket_creation for created tickets."""
        mock_ticket = Mock(id=1)

        with patch('smoothschedule.commerce.tickets.signals._handle_ticket_creation') as mock_handle:
            signals.ticket_notification_handler(sender=Ticket, instance=mock_ticket, created=True)

            mock_handle.assert_called_once_with(mock_ticket)

    def test_calls_handle_ticket_update_for_existing_tickets(self):
        """Should delegate to _handle_ticket_update for updated tickets."""
        mock_ticket = Mock(id=1)

        with patch('smoothschedule.commerce.tickets.signals._handle_ticket_update') as mock_handle:
            signals.ticket_notification_handler(sender=Ticket, instance=mock_ticket, created=False)

            mock_handle.assert_called_once_with(mock_ticket)

    def test_handles_exception_gracefully(self, caplog):
        """Should log error and not raise on exception."""
        mock_ticket = Mock(id=1)

        with patch('smoothschedule.commerce.tickets.signals._handle_ticket_creation', side_effect=Exception("Error")):
            with caplog.at_level(logging.ERROR):
                # Should not raise
                signals.ticket_notification_handler(sender=Ticket, instance=mock_ticket, created=True)

                assert "Error in ticket_notification_handler" in caplog.text
                assert "ticket 1" in caplog.text


class TestSendTicketEmailNotification:
    """Test the _send_ticket_email_notification helper function."""

    @override_settings(TICKET_EMAIL_NOTIFICATIONS_ENABLED=False)
    def test_skips_when_disabled_in_settings(self):
        """Should not send emails when disabled in settings."""
        mock_ticket = Mock(id=1)

        # The function should return early without importing
        signals._send_ticket_email_notification('assigned', mock_ticket)
        # No exception means it returned early

    def test_handles_exception(self, caplog):
        """Should log error on exception."""
        mock_ticket = Mock(id=1)

        with patch.object(signals, '_send_ticket_email_notification') as mock_send:
            # Test the error logging by calling the original and mocking the import to fail
            pass  # The original function handles exceptions internally


class TestSendCommentEmailNotification:
    """Test the _send_comment_email_notification helper function."""

    @override_settings(TICKET_EMAIL_NOTIFICATIONS_ENABLED=False)
    def test_skips_when_disabled_in_settings(self):
        """Should not send emails when disabled in settings."""
        mock_ticket = Mock(id=1)
        mock_comment = Mock(is_internal=False)

        # Should return early without error
        signals._send_comment_email_notification(mock_ticket, mock_comment)

    def test_skips_internal_comments(self):
        """Should not send emails for internal comments."""
        mock_ticket = Mock(id=1)
        mock_comment = Mock(is_internal=True)

        # Should return early without error
        signals._send_comment_email_notification(mock_ticket, mock_comment)


class TestHandleTicketCreation:
    """Test the _handle_ticket_creation helper function."""

    def test_sends_assigned_email_when_assignee_exists(self):
        """Should send assignment email when ticket created with assignee."""
        mock_ticket = Mock(
            id=1,
            assignee_id=5,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=Mock(full_name="John Doe"),
            tenant=Mock(id=1),
            subject="Test",
            priority="high",
            category="bug"
        )

        with patch('smoothschedule.commerce.tickets.signals._send_ticket_email_notification') as mock_email:
            with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[]):
                signals._handle_ticket_creation(mock_ticket)

                mock_email.assert_called_once_with('assigned', mock_ticket)

    def test_notifies_platform_team_for_platform_tickets(self):
        """Should notify platform support team for platform tickets."""
        mock_creator = Mock(full_name="John Doe")
        mock_ticket = Mock(
            id=1,
            assignee_id=None,
            ticket_type=Ticket.TicketType.PLATFORM,
            creator=mock_creator,
            subject="Platform Issue",
            priority="high",
            category="bug"
        )
        mock_support = Mock(id=10)

        with patch('smoothschedule.commerce.tickets.signals.get_platform_support_team', return_value=[mock_support]):
            with patch('smoothschedule.commerce.tickets.signals.create_notification') as mock_notify:
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification') as mock_ws:
                    signals._handle_ticket_creation(mock_ticket)

                    # Verify notification created
                    mock_notify.assert_called_once_with(
                        recipient=mock_support,
                        actor=mock_creator,
                        verb=f"New platform support ticket #1: 'Platform Issue'",
                        action_object=mock_ticket,
                        target=mock_ticket,
                        data={
                            'ticket_id': 1,
                            'subject': "Platform Issue",
                            'priority': "high",
                            'category': "bug"
                        }
                    )

                    # Verify websocket sent
                    mock_ws.assert_called_once()

    def test_notifies_tenant_managers_for_customer_tickets(self):
        """Should notify tenant managers for customer tickets."""
        mock_creator = Mock(full_name="Customer")
        mock_tenant = Mock(id=1)
        mock_ticket = Mock(
            id=2,
            assignee_id=None,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=mock_creator,
            tenant=mock_tenant,
            subject="Help needed",
            priority="normal",
            category="question"
        )
        mock_ticket.get_ticket_type_display.return_value = "Customer"
        mock_manager = Mock(id=20)

        with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[mock_manager]):
            with patch('smoothschedule.commerce.tickets.signals.create_notification') as mock_notify:
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification') as mock_ws:
                    signals._handle_ticket_creation(mock_ticket)

                    # Verify notification created
                    mock_notify.assert_called_once()
                    call_kwargs = mock_notify.call_args[1]
                    assert call_kwargs['recipient'] == mock_manager
                    assert "customer ticket" in call_kwargs['verb'].lower()

    def test_handles_creator_without_full_name(self):
        """Should use 'Someone' when creator has no full_name."""
        mock_ticket = Mock(
            id=1,
            assignee_id=None,
            ticket_type=Ticket.TicketType.PLATFORM,
            creator=None,
            subject="Test",
            priority="high",
            category="bug"
        )

        with patch('smoothschedule.commerce.tickets.signals.get_platform_support_team', return_value=[]):
            # Should not raise
            signals._handle_ticket_creation(mock_ticket)

    def test_handles_exception_gracefully(self, caplog):
        """Should log error and not raise on exception."""
        mock_ticket = Mock(id=1)
        mock_ticket.ticket_type = Ticket.TicketType.PLATFORM

        with patch('smoothschedule.commerce.tickets.signals.get_platform_support_team', side_effect=Exception("Error")):
            with caplog.at_level(logging.ERROR):
                # Should not raise
                signals._handle_ticket_creation(mock_ticket)

                assert "Error handling ticket creation" in caplog.text


class TestHandleTicketUpdate:
    """Test the _handle_ticket_update helper function."""

    def test_sends_assigned_email_when_assignee_changes(self):
        """Should send assignment email when assignee changes."""
        mock_ticket = Mock(
            pk=1,
            id=1,
            assignee_id=5,
            assignee=Mock(id=5),
            status=Ticket.Status.OPEN,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=Mock(id=2),
            tenant=Mock(id=1),
            subject="Test"
        )

        # Set pre-save state with different assignee
        signals._ticket_pre_save_state[1] = {
            'assignee_id': 3,
            'status': Ticket.Status.OPEN
        }

        with patch('smoothschedule.commerce.tickets.signals._send_ticket_email_notification') as mock_email:
            with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[]):
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    with patch('smoothschedule.commerce.tickets.signals.create_notification'):
                        signals._handle_ticket_update(mock_ticket)

                        mock_email.assert_called_with('assigned', mock_ticket)

    def test_sends_resolved_email_when_status_becomes_resolved(self):
        """Should send resolved email when status changes to RESOLVED."""
        mock_ticket = Mock(
            pk=1,
            id=1,
            assignee_id=None,
            assignee=None,
            status=Ticket.Status.RESOLVED,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=Mock(id=2),
            tenant=Mock(id=1),
            subject="Test"
        )

        signals._ticket_pre_save_state[1] = {
            'assignee_id': None,
            'status': Ticket.Status.OPEN
        }

        with patch('smoothschedule.commerce.tickets.signals._send_ticket_email_notification') as mock_email:
            with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[]):
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals._handle_ticket_update(mock_ticket)

                    mock_email.assert_called_with('resolved', mock_ticket)

    def test_sends_resolved_email_when_status_becomes_closed(self):
        """Should send resolved email when status changes to CLOSED."""
        mock_ticket = Mock(
            pk=1,
            id=1,
            assignee_id=None,
            assignee=None,
            status=Ticket.Status.CLOSED,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=Mock(id=2),
            tenant=Mock(id=1),
            subject="Test"
        )

        signals._ticket_pre_save_state[1] = {
            'assignee_id': None,
            'status': Ticket.Status.OPEN
        }

        with patch('smoothschedule.commerce.tickets.signals._send_ticket_email_notification') as mock_email:
            with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[]):
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals._handle_ticket_update(mock_ticket)

                    mock_email.assert_called_with('resolved', mock_ticket)

    def test_sends_status_changed_email_for_other_changes(self):
        """Should send status changed email for non-resolved status changes."""
        mock_ticket = Mock(
            pk=1,
            id=1,
            assignee_id=None,
            assignee=None,
            status=Ticket.Status.IN_PROGRESS,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=Mock(id=2),
            tenant=Mock(id=1),
            subject="Test"
        )

        signals._ticket_pre_save_state[1] = {
            'assignee_id': None,
            'status': Ticket.Status.OPEN
        }

        with patch('smoothschedule.commerce.tickets.signals._send_ticket_email_notification') as mock_email:
            with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[]):
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals._handle_ticket_update(mock_ticket)

                    mock_email.assert_called_with(
                        'status_changed',
                        mock_ticket,
                        old_status=Ticket.Status.OPEN
                    )

    def test_sends_websocket_to_platform_team_for_platform_tickets(self):
        """Should send websocket notifications to platform team."""
        mock_ticket = Mock(
            pk=1,
            id=1,
            assignee_id=None,
            assignee=None,
            status=Ticket.Status.OPEN,
            ticket_type=Ticket.TicketType.PLATFORM,
            creator=Mock(id=2),
            tenant=None,
            subject="Platform Issue",
            priority="high"
        )
        mock_support = Mock(id=10)

        with patch('smoothschedule.commerce.tickets.signals.get_platform_support_team', return_value=[mock_support]):
            with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification') as mock_ws:
                signals._handle_ticket_update(mock_ticket)

                # Should send to platform team member
                calls = mock_ws.call_args_list
                assert any("user_10" in str(call) for call in calls)

    def test_sends_websocket_to_tenant_managers(self):
        """Should send websocket notifications to tenant managers."""
        mock_ticket = Mock(
            pk=1,
            id=1,
            assignee_id=None,
            assignee=None,
            status=Ticket.Status.OPEN,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=Mock(id=2),
            tenant=Mock(id=1),
            subject="Customer Issue",
            priority="normal"
        )
        mock_manager = Mock(id=20)

        with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[mock_manager]):
            with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification') as mock_ws:
                signals._handle_ticket_update(mock_ticket)

                # Should send to tenant manager
                calls = mock_ws.call_args_list
                assert any("user_20" in str(call) for call in calls)

    def test_notifies_assignee(self):
        """Should create notification and send websocket to assignee."""
        mock_assignee = Mock(id=5)
        mock_ticket = Mock(
            pk=1,
            id=1,
            assignee_id=5,
            assignee=mock_assignee,
            status=Ticket.Status.OPEN,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=Mock(id=2),
            tenant=Mock(id=1),
            subject="Test"
        )

        with patch('smoothschedule.commerce.tickets.signals.create_notification') as mock_notify:
            with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification') as mock_ws:
                with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[]):
                    signals._handle_ticket_update(mock_ticket)

                    # Verify notification created for assignee
                    mock_notify.assert_called_once()
                    call_kwargs = mock_notify.call_args[1]
                    assert call_kwargs['recipient'] == mock_assignee

                    # Verify websocket sent to assignee
                    calls = mock_ws.call_args_list
                    assert any("user_5" in str(call) for call in calls)

    def test_handles_no_pre_save_state(self):
        """Should handle update when no pre-save state exists."""
        mock_ticket = Mock(
            pk=999,
            id=999,
            assignee_id=None,
            assignee=None,
            status=Ticket.Status.OPEN,
            ticket_type=Ticket.TicketType.CUSTOMER,
            creator=Mock(id=2),
            tenant=Mock(id=1),
            subject="Test",
            priority="normal"
        )

        with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
            with patch('smoothschedule.commerce.tickets.signals.get_tenant_managers', return_value=[]):
                # Should not raise
                signals._handle_ticket_update(mock_ticket)

    def test_handles_exception_gracefully(self, caplog):
        """Should log error and not raise on exception."""
        mock_ticket = Mock(pk=1, id=1)

        with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification', side_effect=Exception("Error")):
            with caplog.at_level(logging.ERROR):
                # Should not raise
                signals._handle_ticket_update(mock_ticket)

                assert "Error handling ticket update" in caplog.text


class TestCommentNotificationHandler:
    """Test the comment_notification_handler signal receiver."""

    def test_ignores_comment_updates(self):
        """Should not process comment updates, only creation."""
        mock_comment = Mock(id=1)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification') as mock_email:
            signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=False)

            mock_email.assert_not_called()

    def test_sends_email_notification_on_creation(self):
        """Should send email notification when comment is created."""
        mock_ticket = Mock(id=1, creator=Mock(id=2), first_response_at=None)
        mock_author = Mock(id=3, full_name="Support Agent")
        mock_comment = Mock(id=1, ticket=mock_ticket, author=mock_author)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification') as mock_email:
            with patch('smoothschedule.commerce.tickets.signals.create_notification'):
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                    mock_email.assert_called_once_with(mock_ticket, mock_comment)

    def test_sets_first_response_at_when_not_creator(self, caplog):
        """Should set first_response_at when comment is from non-creator."""
        mock_creator = Mock(id=2)
        mock_author = Mock(id=3, full_name="Support")
        mock_ticket = Mock(
            id=1,
            creator=mock_creator,
            first_response_at=None,
            save=Mock()
        )
        mock_comment = Mock(id=1, ticket=mock_ticket, author=mock_author)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.timezone.now', return_value=Mock()):
                with patch('smoothschedule.commerce.tickets.signals.create_notification'):
                    with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                        with caplog.at_level(logging.INFO):
                            signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                            # Verify first_response_at was set
                            assert mock_ticket.first_response_at is not None
                            mock_ticket.save.assert_called_once_with(update_fields=['first_response_at'])
                            assert "Set first_response_at for ticket 1" in caplog.text

    def test_does_not_set_first_response_at_for_creator_comment(self):
        """Should not set first_response_at when creator comments."""
        mock_creator = Mock(id=2)
        mock_ticket = Mock(
            id=1,
            creator=mock_creator,
            first_response_at=None,
            save=Mock()
        )
        mock_comment = Mock(id=1, ticket=mock_ticket, author=mock_creator)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.create_notification'):
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                    # Verify first_response_at was NOT set
                    mock_ticket.save.assert_not_called()

    def test_does_not_overwrite_existing_first_response_at(self):
        """Should not overwrite first_response_at if already set."""
        mock_creator = Mock(id=2)
        mock_author = Mock(id=3)
        existing_time = Mock()
        mock_ticket = Mock(
            id=1,
            creator=mock_creator,
            first_response_at=existing_time,
            save=Mock()
        )
        mock_comment = Mock(id=1, ticket=mock_ticket, author=mock_author)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.create_notification'):
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                    # Verify first_response_at was NOT changed
                    assert mock_ticket.first_response_at == existing_time
                    mock_ticket.save.assert_not_called()

    def test_notifies_ticket_creator(self):
        """Should notify ticket creator about new comment."""
        mock_creator = Mock(id=2)
        mock_author = Mock(id=3, full_name="Support Agent")
        mock_ticket = Mock(
            id=1,
            creator=mock_creator,
            assignee=None,
            first_response_at=Mock(),
            subject="Help"
        )
        mock_comment = Mock(id=10, ticket=mock_ticket, author=mock_author)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.create_notification') as mock_notify:
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification') as mock_ws:
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                    # Verify notification created for creator
                    mock_notify.assert_called()
                    call_kwargs = mock_notify.call_args[1]
                    assert call_kwargs['recipient'] == mock_creator
                    assert "New comment on your ticket" in call_kwargs['verb']

                    # Verify websocket sent to creator
                    calls = mock_ws.call_args_list
                    assert any("user_2" in str(call) for call in calls)

    def test_does_not_notify_creator_if_they_are_author(self):
        """Should not notify creator if they authored the comment."""
        mock_creator = Mock(id=2)
        mock_ticket = Mock(
            id=1,
            creator=mock_creator,
            assignee=None,
            first_response_at=Mock(),
            subject="Help"
        )
        mock_comment = Mock(id=10, ticket=mock_ticket, author=mock_creator)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.create_notification') as mock_notify:
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                    # Verify notification NOT created
                    mock_notify.assert_not_called()

    def test_notifies_assignee(self):
        """Should notify assignee about new comment."""
        mock_creator = Mock(id=2)
        mock_assignee = Mock(id=5)
        mock_author = Mock(id=3, full_name="Customer")
        mock_ticket = Mock(
            id=1,
            creator=mock_creator,
            assignee=mock_assignee,
            first_response_at=Mock(),
            subject="Issue"
        )
        mock_comment = Mock(id=10, ticket=mock_ticket, author=mock_author)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.create_notification') as mock_notify:
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification') as mock_ws:
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                    # Should be called twice: once for creator, once for assignee
                    assert mock_notify.call_count == 2

                    # Verify assignee notification
                    calls = [call[1] for call in mock_notify.call_args_list]
                    assignee_call = [c for c in calls if c['recipient'] == mock_assignee][0]
                    assert "you are assigned to" in assignee_call['verb']

    def test_does_not_notify_assignee_if_they_are_author(self):
        """Should not notify assignee if they authored the comment."""
        mock_creator = Mock(id=2)
        mock_assignee = Mock(id=5)
        mock_ticket = Mock(
            id=1,
            creator=mock_creator,
            assignee=mock_assignee,
            first_response_at=Mock(),
            subject="Issue"
        )
        mock_comment = Mock(id=10, ticket=mock_ticket, author=mock_assignee)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.create_notification') as mock_notify:
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                    # Should only notify creator (not assignee)
                    assert mock_notify.call_count == 1
                    call_kwargs = mock_notify.call_args[1]
                    assert call_kwargs['recipient'] == mock_creator

    def test_does_not_notify_assignee_if_they_are_also_creator(self):
        """Should not notify assignee if they are also the creator."""
        mock_user = Mock(id=2)  # Same user is both creator and assignee
        mock_author = Mock(id=3)
        mock_ticket = Mock(
            id=1,
            creator=mock_user,
            assignee=mock_user,
            first_response_at=Mock(),
            subject="Issue"
        )
        mock_comment = Mock(id=10, ticket=mock_ticket, author=mock_author)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.create_notification') as mock_notify:
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                    # Should only notify once (as creator, not as assignee)
                    assert mock_notify.call_count == 1

    def test_handles_exception_gracefully(self, caplog):
        """Should log error and not raise on exception."""
        mock_comment = Mock(id=1)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification', side_effect=Exception("Error")):
            with caplog.at_level(logging.ERROR):
                # Should not raise
                signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)

                assert "Error in comment_notification_handler" in caplog.text
                assert "comment 1" in caplog.text

    def test_handles_author_without_full_name(self):
        """Should use 'Someone' when author has no full_name."""
        mock_ticket = Mock(
            id=1,
            creator=Mock(id=2),
            assignee=None,
            first_response_at=Mock(),
            subject="Test"
        )
        mock_comment = Mock(id=10, ticket=mock_ticket, author=None)

        with patch('smoothschedule.commerce.tickets.signals._send_comment_email_notification'):
            with patch('smoothschedule.commerce.tickets.signals.create_notification'):
                with patch('smoothschedule.commerce.tickets.signals.send_websocket_notification'):
                    # Should not raise
                    signals.comment_notification_handler(sender=TicketComment, instance=mock_comment, created=True)


class TestSignalRegistration:
    """Test that signals are properly registered."""

    def test_ticket_pre_save_handler_is_registered(self):
        """Should verify ticket_pre_save_handler is connected to pre_save signal."""
        assert callable(signals.ticket_pre_save_handler)

        # Verify it accepts the correct parameters by calling it
        mock_ticket = Mock(pk=None)
        try:
            signals.ticket_pre_save_handler(sender=Ticket, instance=mock_ticket)
            assert True
        except TypeError as e:
            pytest.fail(f"Handler has incorrect signature: {e}")

    def test_ticket_notification_handler_is_registered(self):
        """Should verify ticket_notification_handler is connected to post_save signal."""
        assert callable(signals.ticket_notification_handler)

        # Verify it accepts the correct parameters
        mock_ticket = Mock(id=1, ticket_type=Ticket.TicketType.CUSTOMER)
        with patch('smoothschedule.commerce.tickets.signals._handle_ticket_creation'):
            try:
                signals.ticket_notification_handler(sender=Ticket, instance=mock_ticket, created=True)
                assert True
            except TypeError as e:
                pytest.fail(f"Handler has incorrect signature: {e}")

    def test_comment_notification_handler_is_registered(self):
        """Should verify comment_notification_handler is connected to post_save signal."""
        assert callable(signals.comment_notification_handler)

        # Verify it accepts the correct parameters
        try:
            signals.comment_notification_handler(sender=TicketComment, instance=Mock(id=1), created=False)
            assert True
        except TypeError as e:
            pytest.fail(f"Handler has incorrect signature: {e}")


class TestSignalHandlerSignatures:
    """Test that signal handlers have correct signatures."""

    def test_ticket_pre_save_handler_signature(self):
        """Should accept correct parameters for pre_save signal."""
        # Should not raise TypeError
        signals.ticket_pre_save_handler(
            sender=Ticket,
            instance=Mock(pk=None),
            raw=False,
            using='default',
            update_fields=None
        )

    def test_ticket_notification_handler_signature(self):
        """Should accept correct parameters for post_save signal."""
        mock_ticket = Mock(id=1, ticket_type=Ticket.TicketType.CUSTOMER)

        with patch('smoothschedule.commerce.tickets.signals._handle_ticket_creation'):
            # Should not raise TypeError
            signals.ticket_notification_handler(
                sender=Ticket,
                instance=mock_ticket,
                created=True,
                raw=False,
                using='default',
                update_fields=None
            )

    def test_comment_notification_handler_signature(self):
        """Should accept correct parameters for post_save signal."""
        # Should not raise TypeError (testing signature, not functionality)
        signals.comment_notification_handler(
            sender=TicketComment,
            instance=Mock(id=1),
            created=False,
            raw=False,
            using='default',
            update_fields=None
        )


class TestIsNotificationsAvailableFunction:
    """Test is_notifications_available function behavior."""

    def test_function_exists_and_is_callable(self):
        """Should be a callable function."""
        assert callable(signals.is_notifications_available)

    def test_caches_result_after_first_call(self):
        """Should cache the result."""
        # Set to True and verify it's returned
        signals._notifications_available = True
        result1 = signals.is_notifications_available()
        result2 = signals.is_notifications_available()

        assert result1 is True
        assert result2 is True


class TestCreateNotificationFunction:
    """Test create_notification helper function."""

    def test_function_exists_and_is_callable(self):
        """Should be a callable function."""
        assert callable(signals.create_notification)

    def test_skips_when_notifications_disabled(self):
        """Should skip when notifications app is unavailable."""
        with patch('smoothschedule.commerce.tickets.signals.is_notifications_available', return_value=False):
            # Should not raise
            signals.create_notification(
                recipient=Mock(),
                actor=Mock(),
                verb="test",
                action_object=Mock(),
                target=Mock(),
                data={}
            )


class TestSendTicketEmailNotification:
    """Test _send_ticket_email_notification helper."""

    def test_function_exists_and_is_callable(self):
        """Should be a callable function."""
        assert callable(signals._send_ticket_email_notification)

    def test_skips_when_disabled(self):
        """Should skip when email notifications are disabled."""
        mock_ticket = Mock(id=1)

        with patch('smoothschedule.commerce.tickets.signals.settings') as mock_settings:
            mock_settings.TICKET_EMAIL_NOTIFICATIONS_ENABLED = False

            # Should not raise
            signals._send_ticket_email_notification('assigned', mock_ticket)


class TestSendCommentEmailNotification:
    """Test _send_comment_email_notification helper."""

    def test_function_exists_and_is_callable(self):
        """Should be a callable function."""
        assert callable(signals._send_comment_email_notification)

    def test_skips_internal_comments(self):
        """Should skip email for internal comments."""
        mock_ticket = Mock(id=1)
        mock_comment = Mock(is_internal=True)

        # Should not raise
        signals._send_comment_email_notification(mock_ticket, mock_comment)

    def test_skips_when_disabled(self):
        """Should skip when email notifications are disabled."""
        mock_ticket = Mock(id=1)
        mock_comment = Mock(is_internal=False)

        with patch('smoothschedule.commerce.tickets.signals.settings') as mock_settings:
            mock_settings.TICKET_EMAIL_NOTIFICATIONS_ENABLED = False

            # Should not raise
            signals._send_comment_email_notification(mock_ticket, mock_comment)
