"""
Comprehensive tests for state machine callbacks.

This module tests:
- Pre-transition callbacks
- Post-transition callbacks
- Error callbacks
- Callback execution order
- Callback context handling
"""

from typing import Any
from unittest.mock import Mock, patch

from django.test import TestCase


class CallbackContext:
    """Mock context for testing callbacks."""

    def __init__(
        self, instance: Any = None, from_state: str = "PENDING", to_state: str = "APPROVED", user: Any = None, **extra
    ):
        self.instance = instance or Mock()
        self.from_state = from_state
        self.to_state = to_state
        self.user = user
        self.extra = extra

    def to_dict(self) -> dict[str, Any]:
        return {
            "instance": self.instance,
            "from_state": self.from_state,
            "to_state": self.to_state,
            "user": self.user,
            **self.extra,
        }


class MockCallback:
    """Mock callback for testing."""

    def __init__(self, name: str = "callback", should_raise: bool = False):
        self.name = name
        self.calls: list[dict] = []
        self.should_raise = should_raise

    def __call__(self, context: dict[str, Any]) -> None:
        self.calls.append(context)
        if self.should_raise:
            raise ValueError(f"Callback {self.name} failed")

    @property
    def call_count(self) -> int:
        return len(self.calls)

    def was_called(self) -> bool:
        return len(self.calls) > 0

    def reset(self):
        self.calls = []


class PreTransitionCallbackTests(TestCase):
    """Tests for pre-transition callbacks."""

    def test_pre_callback_executes_before_state_change(self):
        """Test that pre-transition callback executes before state changes."""
        callback = MockCallback("pre_callback")
        context = CallbackContext(from_state="PENDING", to_state="APPROVED")

        # Simulate pre-transition execution
        callback(context.to_dict())

        self.assertTrue(callback.was_called())
        self.assertEqual(callback.calls[0]["from_state"], "PENDING")
        self.assertEqual(callback.calls[0]["to_state"], "APPROVED")

    def test_pre_callback_receives_instance(self):
        """Test that pre-callback receives the model instance."""
        mock_instance = Mock()
        mock_instance.id = 123
        mock_instance.status = "PENDING"

        callback = MockCallback()
        context = CallbackContext(instance=mock_instance)

        callback(context.to_dict())

        self.assertEqual(callback.calls[0]["instance"], mock_instance)

    def test_pre_callback_receives_user(self):
        """Test that pre-callback receives the user performing transition."""
        mock_user = Mock()
        mock_user.username = "moderator"

        callback = MockCallback()
        context = CallbackContext(user=mock_user)

        callback(context.to_dict())

        self.assertEqual(callback.calls[0]["user"], mock_user)

    def test_pre_callback_can_prevent_transition(self):
        """Test that pre-callback can prevent transition by raising exception."""
        callback = MockCallback(should_raise=True)
        context = CallbackContext()

        with self.assertRaises(ValueError):
            callback(context.to_dict())

    def test_multiple_pre_callbacks_execute_in_order(self):
        """Test that multiple pre-callbacks execute in registration order."""
        execution_order = []

        def callback_1(ctx):
            execution_order.append("first")

        def callback_2(ctx):
            execution_order.append("second")

        def callback_3(ctx):
            execution_order.append("third")

        context = CallbackContext().to_dict()

        # Execute in order
        callback_1(context)
        callback_2(context)
        callback_3(context)

        self.assertEqual(execution_order, ["first", "second", "third"])


class PostTransitionCallbackTests(TestCase):
    """Tests for post-transition callbacks."""

    def test_post_callback_executes_after_state_change(self):
        """Test that post-transition callback executes after state changes."""
        callback = MockCallback("post_callback")

        # Simulate instance after transition
        mock_instance = Mock()
        mock_instance.status = "APPROVED"  # Already changed

        context = CallbackContext(instance=mock_instance, from_state="PENDING", to_state="APPROVED")

        callback(context.to_dict())

        self.assertTrue(callback.was_called())
        self.assertEqual(callback.calls[0]["instance"].status, "APPROVED")

    def test_post_callback_receives_updated_instance(self):
        """Test that post-callback receives instance with new state."""
        mock_instance = Mock()
        mock_instance.status = "APPROVED"
        mock_instance.approved_at = "2025-01-15"
        mock_instance.handled_by_id = 456

        callback = MockCallback()
        context = CallbackContext(instance=mock_instance)

        callback(context.to_dict())

        instance = callback.calls[0]["instance"]
        self.assertEqual(instance.status, "APPROVED")
        self.assertEqual(instance.approved_at, "2025-01-15")

    def test_post_callback_failure_does_not_rollback(self):
        """Test that post-callback failures don't rollback the transition."""
        # In a real scenario, the transition would already be committed
        callback = MockCallback(should_raise=True)
        context = CallbackContext()

        # Post-callback failure should not affect already-committed transition
        with self.assertRaises(ValueError):
            callback(context.to_dict())

        # The transition would still be committed in real usage
        self.assertTrue(callback.was_called())

    def test_multiple_post_callbacks_execute_in_order(self):
        """Test that multiple post-callbacks execute in order."""
        execution_order = []

        def notification_callback(ctx):
            execution_order.append("notification")

        def cache_callback(ctx):
            execution_order.append("cache")

        def analytics_callback(ctx):
            execution_order.append("analytics")

        context = CallbackContext().to_dict()

        notification_callback(context)
        cache_callback(context)
        analytics_callback(context)

        self.assertEqual(execution_order, ["notification", "cache", "analytics"])


class ErrorCallbackTests(TestCase):
    """Tests for error callbacks."""

    def test_error_callback_receives_exception(self):
        """Test that error callback receives exception information."""
        error_callback = MockCallback()

        try:
            raise ValueError("Transition failed")
        except ValueError as e:
            error_context = {
                "instance": Mock(),
                "from_state": "PENDING",
                "to_state": "APPROVED",
                "exception": e,
                "exception_type": type(e).__name__,
            }
            error_callback(error_context)

        self.assertTrue(error_callback.was_called())
        self.assertIn("exception", error_callback.calls[0])
        self.assertEqual(error_callback.calls[0]["exception_type"], "ValueError")

    def test_error_callback_for_cleanup(self):
        """Test that error callbacks can perform cleanup."""
        cleanup_performed = []

        def cleanup_callback(ctx):
            cleanup_performed.append(True)
            # In real usage, might release locks, revert partial changes, etc.

        try:
            raise ValueError("Transition failed")
        except ValueError:
            cleanup_callback({"exception": "test"})

        self.assertTrue(cleanup_performed)

    def test_error_callback_receives_context(self):
        """Test that error callback receives full transition context."""
        mock_instance = Mock()
        mock_user = Mock()

        error_callback = MockCallback()

        error_context = {
            "instance": mock_instance,
            "from_state": "PENDING",
            "to_state": "APPROVED",
            "user": mock_user,
            "exception": ValueError("Test error"),
        }

        error_callback(error_context)

        self.assertEqual(error_callback.calls[0]["instance"], mock_instance)
        self.assertEqual(error_callback.calls[0]["user"], mock_user)


class ConditionalCallbackTests(TestCase):
    """Tests for conditional callback execution."""

    def test_callback_with_state_filter(self):
        """Test callback that only executes for specific states."""
        execution_log = []

        def approval_only_callback(ctx):
            if ctx.get("to_state") == "APPROVED":
                execution_log.append("approved")

        # Transition to APPROVED - should execute
        approval_only_callback({"to_state": "APPROVED"})
        self.assertEqual(len(execution_log), 1)

        # Transition to REJECTED - should not execute
        approval_only_callback({"to_state": "REJECTED"})
        self.assertEqual(len(execution_log), 1)  # Still 1

    def test_callback_with_transition_filter(self):
        """Test callback that only executes for specific transitions."""
        execution_log = []

        def escalation_callback(ctx):
            if ctx.get("to_state") == "ESCALATED":
                execution_log.append("escalated")

        # Escalation - should execute
        escalation_callback({"to_state": "ESCALATED"})
        self.assertEqual(len(execution_log), 1)

        # Other transitions - should not execute
        escalation_callback({"to_state": "APPROVED"})
        self.assertEqual(len(execution_log), 1)

    def test_callback_with_user_role_filter(self):
        """Test callback that checks user role."""
        admin_notifications = []

        def admin_only_notification(ctx):
            user = ctx.get("user")
            if user and getattr(user, "role", None) == "ADMIN":
                admin_notifications.append(ctx)

        admin_user = Mock(role="ADMIN")
        moderator_user = Mock(role="MODERATOR")

        admin_only_notification({"user": admin_user})
        self.assertEqual(len(admin_notifications), 1)

        admin_only_notification({"user": moderator_user})
        self.assertEqual(len(admin_notifications), 1)  # Still 1


class CallbackChainTests(TestCase):
    """Tests for callback chains and pipelines."""

    def test_callback_chain_continues_on_success(self):
        """Test that callback chain continues when callbacks succeed."""
        results = []

        callbacks = [
            lambda ctx: results.append("a"),
            lambda ctx: results.append("b"),
            lambda ctx: results.append("c"),
        ]

        context = {}
        for cb in callbacks:
            cb(context)

        self.assertEqual(results, ["a", "b", "c"])

    def test_callback_chain_stops_on_failure(self):
        """Test that callback chain stops when a callback fails."""
        results = []

        def callback_a(ctx):
            results.append("a")

        def callback_b(ctx):
            raise ValueError("B failed")

        def callback_c(ctx):
            results.append("c")

        callbacks = [callback_a, callback_b, callback_c]

        context = {}
        for cb in callbacks:
            try:
                cb(context)
            except ValueError:
                break

        self.assertEqual(results, ["a"])  # c never executed

    def test_callback_chain_with_continue_on_error(self):
        """Test callback chain that continues despite errors."""
        results = []
        errors = []

        def callback_a(ctx):
            results.append("a")

        def callback_b(ctx):
            raise ValueError("B failed")

        def callback_c(ctx):
            results.append("c")

        callbacks = [callback_a, callback_b, callback_c]

        context = {}
        for cb in callbacks:
            try:
                cb(context)
            except Exception as e:
                errors.append(str(e))

        self.assertEqual(results, ["a", "c"])
        self.assertEqual(len(errors), 1)


class CallbackContextEnrichmentTests(TestCase):
    """Tests for callback context enrichment."""

    def test_context_includes_model_class(self):
        """Test that context includes the model class."""
        mock_instance = Mock()
        mock_instance.__class__.__name__ = "EditSubmission"

        context = {"instance": mock_instance, "model_class": type(mock_instance)}

        self.assertIn("model_class", context)

    def test_context_includes_transition_name(self):
        """Test that context includes the transition method name."""
        context = {
            "instance": Mock(),
            "from_state": "PENDING",
            "to_state": "APPROVED",
            "transition_name": "transition_to_approved",
        }

        self.assertEqual(context["transition_name"], "transition_to_approved")

    def test_context_includes_timestamp(self):
        """Test that context includes transition timestamp."""
        from django.utils import timezone

        context = {"instance": Mock(), "timestamp": timezone.now()}

        self.assertIn("timestamp", context)


# ============================================================================
# Notification Callback Tests
# ============================================================================


class NotificationCallbackTests(TestCase):
    """Tests for NotificationCallback and ModerationNotificationCallback."""

    def setUp(self):
        """Set up mock notification service."""
        self.notification_service = Mock()
        self.notification_service.send_notification = Mock(return_value=True)

    def _create_transition_context(
        self,
        model_name: str = "EditSubmission",
        source_state: str = "PENDING",
        target_state: str = "APPROVED",
        user=None,
        instance=None,
    ):
        """Helper to create a TransitionContext."""
        from django.utils import timezone

        from ..callback_base import TransitionContext

        if instance is None:
            instance = Mock()
            instance.pk = 123
            instance.__class__.__name__ = model_name

        if user is None:
            user = Mock()
            user.pk = 1
            user.username = "moderator"

        return TransitionContext(
            instance=instance,
            field_name="status",
            source_state=source_state,
            target_state=target_state,
            user=user,
            timestamp=timezone.now(),
        )

    @patch("apps.core.state_machine.callbacks.notifications.NotificationService")
    def test_notification_callback_approval_title(self, mock_service_class):
        """Test NotificationCallback generates correct title for approvals."""
        from ..callbacks.notifications import NotificationCallback

        mock_service = Mock()
        mock_service.send_notification = Mock(return_value=True)
        mock_service_class.return_value = mock_service

        callback = NotificationCallback()
        context = self._create_transition_context(
            source_state="PENDING",
            target_state="APPROVED",
        )

        callback.execute(context)

        # Check that notification was sent with correct title
        if mock_service.send_notification.called:
            call_args = mock_service.send_notification.call_args
            self.assertIn("approved", call_args[1].get("title", "").lower())

    @patch("apps.core.state_machine.callbacks.notifications.NotificationService")
    def test_notification_callback_rejection_title(self, mock_service_class):
        """Test NotificationCallback generates correct title for rejections."""
        from ..callbacks.notifications import NotificationCallback

        mock_service = Mock()
        mock_service.send_notification = Mock(return_value=True)
        mock_service_class.return_value = mock_service

        callback = NotificationCallback()
        context = self._create_transition_context(
            source_state="PENDING",
            target_state="REJECTED",
        )

        callback.execute(context)

        if mock_service.send_notification.called:
            call_args = mock_service.send_notification.call_args
            self.assertIn("rejected", call_args[1].get("title", "").lower())

    @patch("apps.core.state_machine.callbacks.notifications.NotificationService")
    def test_moderation_notification_recipient_selection(self, mock_service_class):
        """Test ModerationNotificationCallback sends to correct recipient."""
        from ..callbacks.notifications import ModerationNotificationCallback

        mock_service = Mock()
        mock_service.send_notification = Mock(return_value=True)
        mock_service_class.return_value = mock_service

        submitter = Mock()
        submitter.pk = 999
        submitter.username = "submitter"

        instance = Mock()
        instance.pk = 123
        instance.__class__.__name__ = "EditSubmission"
        instance.user = submitter  # The submitter who should receive notification

        callback = ModerationNotificationCallback()
        context = self._create_transition_context(
            target_state="APPROVED",
            instance=instance,
        )

        callback.execute(context)

        if mock_service.send_notification.called:
            call_args = mock_service.send_notification.call_args
            # Should notify the submitter about their submission
            recipient = call_args[1].get("user") or call_args[0][0] if call_args[0] else None
            self.assertIsNotNone(recipient)

    @patch("apps.core.state_machine.callbacks.notifications.NotificationService")
    def test_notification_callback_handles_service_error(self, mock_service_class):
        """Test NotificationCallback handles service errors gracefully."""
        from ..callbacks.notifications import NotificationCallback

        mock_service = Mock()
        mock_service.send_notification = Mock(side_effect=Exception("Service unavailable"))
        mock_service_class.return_value = mock_service

        callback = NotificationCallback()
        context = self._create_transition_context()

        # Should not raise exception
        result = callback.execute(context)
        # Callback may return False on error but should not raise
        self.assertIsNotNone(result)

    @patch("apps.core.state_machine.callbacks.notifications.NotificationService")
    def test_notification_callback_message_includes_model_info(self, mock_service_class):
        """Test notification message includes model information."""
        from ..callbacks.notifications import NotificationCallback

        mock_service = Mock()
        mock_service.send_notification = Mock(return_value=True)
        mock_service_class.return_value = mock_service

        callback = NotificationCallback()
        context = self._create_transition_context(model_name="PhotoSubmission")

        callback.execute(context)

        if mock_service.send_notification.called:
            call_args = mock_service.send_notification.call_args
            message = call_args[1].get("message", "")
            # Should reference the submission type or model
            self.assertIsInstance(message, str)


# ============================================================================
# Cache Callback Tests
# ============================================================================


class CacheCallbackTests(TestCase):
    """Tests for cache invalidation callbacks."""

    def _create_transition_context(
        self,
        model_name: str = "Park",
        instance_id: int = 123,
        source_state: str = "OPERATING",
        target_state: str = "CLOSED_TEMP",
    ):
        """Helper to create a TransitionContext."""
        from django.utils import timezone

        from ..callback_base import TransitionContext

        instance = Mock()
        instance.pk = instance_id
        instance.__class__.__name__ = model_name

        return TransitionContext(
            instance=instance,
            field_name="status",
            source_state=source_state,
            target_state=target_state,
            user=Mock(),
            timestamp=timezone.now(),
        )

    @patch("apps.core.state_machine.callbacks.cache.CacheInvalidationCallback._get_cache_service")
    def test_cache_callback_invalidates_model_patterns(self, mock_get_service):
        """Test CacheInvalidationCallback invalidates correct patterns."""
        from ..callbacks.cache import CacheInvalidationCallback

        mock_cache = Mock()
        mock_cache.invalidate_pattern = Mock()
        mock_get_service.return_value = mock_cache

        callback = CacheInvalidationCallback(patterns=["*park:123*", "*parks*"])
        context = self._create_transition_context()

        callback.execute(context)

        # Should have called invalidate_pattern for each pattern
        self.assertTrue(mock_cache.invalidate_pattern.called)

    @patch("apps.core.state_machine.callbacks.cache.CacheInvalidationCallback._get_cache_service")
    def test_cache_callback_generates_instance_patterns(self, mock_get_service):
        """Test CacheInvalidationCallback generates instance-specific patterns."""
        from ..callbacks.cache import CacheInvalidationCallback

        mock_cache = Mock()
        mock_cache.invalidate_pattern = Mock()
        mock_get_service.return_value = mock_cache

        callback = CacheInvalidationCallback(include_instance_patterns=True)
        context = self._create_transition_context(model_name="Park", instance_id=456)

        callback.execute(context)

        # Should have called invalidate_pattern with instance-specific patterns
        self.assertTrue(mock_cache.invalidate_pattern.called)
        patterns_called = [call[0][0] for call in mock_cache.invalidate_pattern.call_args_list]
        # Should include patterns containing the instance ID
        has_instance_pattern = any("456" in p for p in patterns_called)
        self.assertTrue(has_instance_pattern, f"No pattern with instance ID in {patterns_called}")

    @patch("apps.core.state_machine.callbacks.cache.CacheInvalidationCallback._get_cache_service")
    def test_cache_callback_handles_service_unavailable(self, mock_get_service):
        """Test CacheInvalidationCallback handles unavailable cache service."""
        from ..callbacks.cache import CacheInvalidationCallback

        mock_get_service.return_value = None

        callback = CacheInvalidationCallback(patterns=["*test*"])
        context = self._create_transition_context()

        # Should not raise, uses fallback
        result = callback.execute(context)
        # Should return True (fallback succeeds)
        self.assertTrue(result)

    @patch("apps.core.state_machine.callbacks.cache.CacheInvalidationCallback._get_cache_service")
    def test_cache_callback_continues_on_pattern_error(self, mock_get_service):
        """Test CacheInvalidationCallback continues if individual pattern fails."""
        from ..callbacks.cache import CacheInvalidationCallback

        mock_cache = Mock()
        call_count = 0

        def invalidate_side_effect(pattern):
            nonlocal call_count
            call_count += 1
            if "bad" in pattern:
                raise Exception("Pattern invalid")

        mock_cache.invalidate_pattern = Mock(side_effect=invalidate_side_effect)
        mock_get_service.return_value = mock_cache

        callback = CacheInvalidationCallback(patterns=["good:*", "bad:*", "another:*"], include_instance_patterns=False)
        context = self._create_transition_context()

        # Should not raise overall
        callback.execute(context)
        # All patterns should have been attempted
        self.assertGreater(call_count, 1)


class ModelCacheInvalidationTests(TestCase):
    """Tests for model-specific cache invalidation."""

    def _create_transition_context(
        self,
        model_name: str = "Ride",
        instance_id: int = 789,
    ):
        from django.utils import timezone

        from ..callback_base import TransitionContext

        instance = Mock()
        instance.pk = instance_id
        instance.__class__.__name__ = model_name

        # Add park reference for rides
        if model_name == "Ride":
            instance.park = Mock()
            instance.park.pk = 111

        return TransitionContext(
            instance=instance,
            field_name="status",
            source_state="OPERATING",
            target_state="CLOSED_TEMP",
            user=Mock(),
            timestamp=timezone.now(),
        )

    @patch("apps.core.state_machine.callbacks.cache.CacheInvalidationCallback._get_cache_service")
    def test_ride_cache_includes_park_patterns(self, mock_get_service):
        """Test RideCacheInvalidation includes parent park patterns."""
        from ..callbacks.cache import RideCacheInvalidation

        mock_cache = Mock()
        mock_cache.invalidate_pattern = Mock()
        mock_get_service.return_value = mock_cache

        callback = RideCacheInvalidation()
        context = self._create_transition_context()

        callback.execute(context)

        patterns_called = [call[0][0] for call in mock_cache.invalidate_pattern.call_args_list]

        # Should include park patterns (parent park ID is 111)
        has_park_pattern = any("park" in p.lower() for p in patterns_called)
        self.assertTrue(has_park_pattern, f"No park pattern in {patterns_called}")


# ============================================================================
# Related Update Callback Tests
# ============================================================================


class RelatedUpdateCallbackTests(TestCase):
    """Tests for related model update callbacks."""

    def setUp(self):
        """Set up test fixtures."""
        from django.contrib.auth import get_user_model

        get_user_model()

        self.user = Mock()
        self.user.pk = 1
        self.user.username = "testuser"

    def _create_transition_context(
        self,
        model_name: str = "Ride",
        instance=None,
        target_state: str = "OPERATING",
    ):
        from django.utils import timezone

        from ..callback_base import TransitionContext

        if instance is None:
            instance = Mock()
            instance.pk = 123
            instance.__class__.__name__ = model_name

        return TransitionContext(
            instance=instance,
            field_name="status",
            source_state="UNDER_CONSTRUCTION",
            target_state=target_state,
            user=self.user,
            timestamp=timezone.now(),
        )

    def test_park_count_update_callback_updates_counts(self):
        """Test ParkCountUpdateCallback updates ride_count and coaster_count."""
        from ..callbacks.related_updates import ParkCountUpdateCallback

        # Create mock park with rides
        mock_park = Mock()
        mock_park.pk = 100
        mock_park.ride_count = 5
        mock_park.coaster_count = 2
        mock_park.save = Mock()

        # Create mock ride that belongs to park
        mock_ride = Mock()
        mock_ride.pk = 200
        mock_ride.__class__.__name__ = "Ride"
        mock_ride.park = mock_park
        mock_ride.is_coaster = True

        # Mock the queryset for counting
        mock_park.rides = Mock()
        mock_park.rides.filter = Mock(return_value=Mock(count=Mock(return_value=6)))
        mock_park.rides.filter.return_value.count = Mock(return_value=6)

        callback = ParkCountUpdateCallback()
        context = self._create_transition_context(
            model_name="Ride",
            instance=mock_ride,
            target_state="OPERATING",
        )

        # Execute callback
        result = callback.execute(context)

        # Callback should execute without error
        self.assertTrue(result)

    def test_park_count_update_callback_handles_missing_park(self):
        """Test ParkCountUpdateCallback handles rides without park reference."""
        from ..callbacks.related_updates import ParkCountUpdateCallback

        mock_ride = Mock()
        mock_ride.pk = 200
        mock_ride.__class__.__name__ = "Ride"
        mock_ride.park = None  # No park

        callback = ParkCountUpdateCallback()
        context = self._create_transition_context(
            model_name="Ride",
            instance=mock_ride,
        )

        # Should not raise, should handle gracefully
        result = callback.execute(context)
        self.assertIsNotNone(result)

    def test_park_count_update_callback_updates_on_open(self):
        """Test counts update when ride opens (UNDER_CONSTRUCTION -> OPERATING)."""
        from ..callbacks.related_updates import ParkCountUpdateCallback

        mock_park = Mock()
        mock_park.pk = 100
        mock_park.rides = Mock()
        mock_park.rides.filter = Mock(return_value=Mock(count=Mock(return_value=10)))
        mock_park.save = Mock()

        mock_ride = Mock()
        mock_ride.pk = 200
        mock_ride.__class__.__name__ = "Ride"
        mock_ride.park = mock_park
        mock_ride.is_coaster = False

        callback = ParkCountUpdateCallback()
        context = self._create_transition_context(
            model_name="Ride",
            instance=mock_ride,
            target_state="OPERATING",
        )

        callback.execute(context)

        # Park should be updated
        # (verification depends on implementation details)
        self.assertTrue(True)  # Callback executed without error

    def test_park_count_update_callback_updates_on_close(self):
        """Test counts update when ride closes (OPERATING -> CLOSED_PERM)."""
        from ..callbacks.related_updates import ParkCountUpdateCallback

        mock_park = Mock()
        mock_park.pk = 100
        mock_park.rides = Mock()
        mock_park.rides.filter = Mock(return_value=Mock(count=Mock(return_value=8)))
        mock_park.save = Mock()

        mock_ride = Mock()
        mock_ride.pk = 200
        mock_ride.__class__.__name__ = "Ride"
        mock_ride.park = mock_park
        mock_ride.is_coaster = True

        callback = ParkCountUpdateCallback()
        context = self._create_transition_context(
            model_name="Ride",
            instance=mock_ride,
            target_state="CLOSED_PERM",
        )

        result = callback.execute(context)
        self.assertTrue(result)


# ============================================================================
# Callback Error Handling Tests
# ============================================================================


class CallbackErrorHandlingTests(TestCase):
    """Tests for callback error handling paths."""

    def _create_transition_context(self):
        from django.utils import timezone

        from ..callback_base import TransitionContext

        instance = Mock()
        instance.pk = 1
        instance.__class__.__name__ = "EditSubmission"

        return TransitionContext(
            instance=instance,
            field_name="status",
            source_state="PENDING",
            target_state="APPROVED",
            user=Mock(),
            timestamp=timezone.now(),
        )

    @patch("apps.core.state_machine.callbacks.notifications.NotificationService")
    def test_notification_callback_logs_error_on_failure(self, mock_service_class):
        """Test NotificationCallback logs errors when service fails."""
        import logging

        from ..callbacks.notifications import NotificationCallback

        mock_service = Mock()
        mock_service.send_notification = Mock(side_effect=Exception("Network error"))
        mock_service_class.return_value = mock_service

        callback = NotificationCallback()
        context = self._create_transition_context()

        with self.assertLogs(level=logging.WARNING):
            try:  # noqa: SIM105
                callback.execute(context)
            except Exception:
                pass  # May or may not raise depending on implementation

            # Should have logged something about the error
            # (Logging behavior depends on implementation)

    @patch("apps.core.state_machine.callbacks.cache.CacheInvalidationCallback._get_cache_service")
    def test_cache_callback_returns_false_on_total_failure(self, mock_get_service):
        """Test CacheInvalidationCallback returns False on complete failure."""
        from ..callbacks.cache import CacheInvalidationCallback

        mock_cache = Mock()
        mock_cache.invalidate_pattern = Mock(side_effect=Exception("Cache error"))
        mock_get_service.return_value = mock_cache

        callback = CacheInvalidationCallback(patterns=["*test*"], include_instance_patterns=False)
        context = self._create_transition_context()

        result = callback.execute(context)
        # On complete failure, should return False
        self.assertFalse(result)

    def test_callback_with_none_user(self):
        """Test callbacks handle None user gracefully."""
        from django.utils import timezone

        from ..callback_base import TransitionContext
        from ..callbacks.notifications import NotificationCallback

        instance = Mock()
        instance.pk = 1
        instance.__class__.__name__ = "EditSubmission"

        context = TransitionContext(
            instance=instance,
            field_name="status",
            source_state="PENDING",
            target_state="APPROVED",
            user=None,  # No user
            timestamp=timezone.now(),
        )

        with patch("apps.core.state_machine.callbacks.notifications.NotificationService"):
            callback = NotificationCallback()
            # Should not raise with None user
            try:
                callback.execute(context)
            except Exception as e:
                self.fail(f"Callback raised exception with None user: {e}")
