"""Tests for the hook system core functionality."""

from decimal import Decimal
from django.test import TestCase

from wallet_utils import (
    HookManager,
    HookRegistry,
    HookType,
    PostHookError,
)
from wallet_utils.hooks import HookContext
from wallet_utils.exceptions import HookRejectionError, HookExecutionError


class TestHookRegistry(TestCase):
    """Tests for HookRegistry class."""

    def test_register_hook_basic(self):
        """Test registering a basic hook."""
        registry = HookRegistry()

        def my_hook(context: HookContext) -> bool:
            return True

        registry.register(
            name="test_hook",
            hook_type=HookType.PRE,
            callback=my_hook,
            operation="add",
            priority=100,
        )

        hooks = registry.get_hooks("add", HookType.PRE)
        assert len(hooks) == 1
        assert hooks[0].name == "test_hook"
        assert hooks[0].priority == 100

    def test_register_hook_with_priority(self):
        """Test that hooks are sorted by priority."""
        registry = HookRegistry()

        def hook1(context: HookContext) -> bool:
            return True

        def hook2(context: HookContext) -> bool:
            return True

        def hook3(context: HookContext) -> bool:
            return True

        # Register in random order
        registry.register("hook_low", HookType.PRE, hook1, "add", priority=500)
        registry.register("hook_high", HookType.PRE, hook2, "add", priority=10)
        registry.register("hook_medium", HookType.PRE, hook3, "add", priority=100)

        hooks = registry.get_hooks("add", HookType.PRE)
        assert len(hooks) == 3
        assert hooks[0].name == "hook_high"  # priority 10
        assert hooks[1].name == "hook_medium"  # priority 100
        assert hooks[2].name == "hook_low"  # priority 500

    def test_register_global_hook(self):
        """Test registering a global hook that applies to all operations."""
        registry = HookRegistry()

        def global_hook(context: HookContext) -> bool:
            return True

        registry.register(
            name="global_hook",
            hook_type=HookType.PRE,
            callback=global_hook,
            operation="*",
            priority=100,
        )

        # Global hook should appear for all operations
        for operation in ["add", "deduct", "transfer"]:
            hooks = registry.get_hooks(operation, HookType.PRE)
            assert len(hooks) == 1
            assert hooks[0].name == "global_hook"

    def test_register_duplicate_name_raises_error(self):
        """Test that registering a hook with duplicate name raises error."""
        registry = HookRegistry()

        def hook1(context: HookContext) -> bool:
            return True

        registry.register("duplicate", HookType.PRE, hook1, "add")

        # Try to register with same name
        with self.assertRaises(ValueError) as cm:
            registry.register("duplicate", HookType.PRE, hook1, "deduct")
        self.assertIn("already registered", str(cm.exception))

    def test_register_invalid_operation_raises_error(self):
        """Test that registering with invalid operation raises error."""
        registry = HookRegistry()

        def hook1(context: HookContext) -> bool:
            return True

        with self.assertRaises(ValueError) as cm:
            registry.register("test", HookType.PRE, hook1, "invalid_op")
        self.assertIn("Invalid operation", str(cm.exception))

    def test_unregister_hook(self):
        """Test unregistering a hook."""
        registry = HookRegistry()

        def my_hook(context: HookContext) -> bool:
            return True

        registry.register("test_hook", HookType.PRE, my_hook, "add")
        assert len(registry.get_hooks("add", HookType.PRE)) == 1

        # Unregister
        result = registry.unregister("test_hook")
        assert result is True
        assert len(registry.get_hooks("add", HookType.PRE)) == 0

    def test_unregister_nonexistent_hook(self):
        """Test unregistering a hook that doesn't exist."""
        registry = HookRegistry()
        result = registry.unregister("nonexistent")
        assert result is False

    def test_get_hooks_filters_by_type(self):
        """Test that get_hooks filters by hook type."""
        registry = HookRegistry()

        def pre_hook(context: HookContext) -> bool:
            return True

        def post_hook(context: HookContext) -> None:
            pass

        registry.register("pre_hook", HookType.PRE, pre_hook, "add")
        registry.register("post_hook", HookType.POST, post_hook, "add")

        pre_hooks = registry.get_hooks("add", HookType.PRE)
        post_hooks = registry.get_hooks("add", HookType.POST)

        assert len(pre_hooks) == 1
        assert pre_hooks[0].name == "pre_hook"

        assert len(post_hooks) == 1
        assert post_hooks[0].name == "post_hook"

    def test_get_hooks_combines_operation_and_global(self):
        """Test that get_hooks combines operation-specific and global hooks."""
        registry = HookRegistry()

        def hook1(context: HookContext) -> bool:
            return True

        def hook2(context: HookContext) -> bool:
            return True

        registry.register("specific_hook", HookType.PRE, hook1, "add", priority=100)
        registry.register("global_hook", HookType.PRE, hook2, "*", priority=50)

        hooks = registry.get_hooks("add", HookType.PRE)
        assert len(hooks) == 2
        # Global hook has higher priority (50 < 100)
        assert hooks[0].name == "global_hook"
        assert hooks[1].name == "specific_hook"

    def test_clear_hooks(self):
        """Test clearing all hooks."""
        registry = HookRegistry()

        def hook1(context: HookContext) -> bool:
            return True

        registry.register("hook1", HookType.PRE, hook1, "add")
        registry.register("hook2", HookType.POST, hook1, "deduct")

        registry.clear()

        assert len(registry.get_hooks("add", HookType.PRE)) == 0
        assert len(registry.get_hooks("deduct", HookType.POST)) == 0


class TestHookManager(TestCase):
    """Tests for HookManager class."""

    def test_execute_pre_hooks_success(self):
        """Test executing PRE hooks successfully."""
        registry = HookRegistry()
        manager = HookManager(registry)

        executed = []

        def hook1(context: HookContext) -> bool:
            executed.append("hook1")
            return True

        def hook2(context: HookContext) -> bool:
            executed.append("hook2")
            return True

        registry.register("hook1", HookType.PRE, hook1, "add", priority=10)
        registry.register("hook2", HookType.PRE, hook2, "add", priority=20)

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        manager.execute_pre_hooks(context)

        # Both hooks should execute in priority order
        assert executed == ["hook1", "hook2"]

    def test_execute_pre_hooks_rejection_false(self):
        """Test PRE hook rejecting transaction by returning False."""
        registry = HookRegistry()
        manager = HookManager(registry)

        def rejecting_hook(context: HookContext) -> bool:
            return False

        registry.register("rejecting_hook", HookType.PRE, rejecting_hook, "add")

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        with self.assertRaises(HookRejectionError) as cm:
            manager.execute_pre_hooks(context)

        self.assertIn("HOOK_REJECTED_REJECTING_HOOK", cm.exception.error_code)

    def test_execute_pre_hooks_rejection_exception(self):
        """Test PRE hook rejecting transaction by raising HookRejectionError."""
        registry = HookRegistry()
        manager = HookManager(registry)

        def rejecting_hook(context: HookContext) -> bool:
            raise HookRejectionError(
                error_code="CUSTOM_ERROR",
                message="Transaction not allowed",
                details={"reason": "test"},
            )

        registry.register("rejecting_hook", HookType.PRE, rejecting_hook, "add")

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        with self.assertRaises(HookRejectionError) as cm:
            manager.execute_pre_hooks(context)

        self.assertEqual(cm.exception.error_code, "CUSTOM_ERROR")
        self.assertEqual(cm.exception.message, "Transaction not allowed")
        self.assertEqual(cm.exception.details["reason"], "test")

    def test_execute_pre_hooks_unexpected_error(self):
        """Test PRE hook raising unexpected exception."""
        registry = HookRegistry()
        manager = HookManager(registry)

        def failing_hook(context: HookContext) -> bool:
            raise ValueError("Something went wrong")

        registry.register("failing_hook", HookType.PRE, failing_hook, "add")

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        with self.assertRaises(HookExecutionError) as cm:
            manager.execute_pre_hooks(context)

        self.assertEqual(cm.exception.hook_name, "failing_hook")
        self.assertIn("Something went wrong", str(cm.exception))

    def test_execute_pre_hooks_stops_on_rejection(self):
        """Test that PRE hook execution stops when a hook rejects."""
        registry = HookRegistry()
        manager = HookManager(registry)

        executed = []

        def hook1(context: HookContext) -> bool:
            executed.append("hook1")
            return True

        def rejecting_hook(context: HookContext) -> bool:
            executed.append("rejecting")
            return False

        def hook3(context: HookContext) -> bool:
            executed.append("hook3")
            return True

        registry.register("hook1", HookType.PRE, hook1, "add", priority=10)
        registry.register("rejecting", HookType.PRE, rejecting_hook, "add", priority=20)
        registry.register("hook3", HookType.PRE, hook3, "add", priority=30)

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        with self.assertRaises(HookRejectionError):
            manager.execute_pre_hooks(context)

        # Only first two hooks should execute
        self.assertEqual(executed, ["hook1", "rejecting"])

    def test_execute_post_hooks_success(self):
        """Test executing POST hooks successfully."""
        registry = HookRegistry()
        manager = HookManager(registry)

        executed = []

        def hook1(context: HookContext) -> None:
            executed.append("hook1")

        def hook2(context: HookContext) -> None:
            executed.append("hook2")

        registry.register("hook1", HookType.POST, hook1, "add", priority=10)
        registry.register("hook2", HookType.POST, hook2, "add", priority=20)

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        errors = manager.execute_post_hooks(context)

        assert len(errors) == 0
        assert executed == ["hook1", "hook2"]

    def test_execute_post_hooks_captures_errors(self):
        """Test that POST hook errors are captured, not raised."""
        registry = HookRegistry()
        manager = HookManager(registry)

        def failing_hook(context: HookContext) -> None:
            raise ValueError("POST hook failed")

        registry.register("failing_hook", HookType.POST, failing_hook, "add")

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        errors = manager.execute_post_hooks(context)

        assert len(errors) == 1
        assert errors[0].hook_name == "failing_hook"
        assert errors[0].error_code == "HOOK_EXECUTION_ERROR"
        assert "POST hook failed" in errors[0].message

    def test_execute_post_hooks_cannot_reject(self):
        """Test that POST hooks trying to reject are captured as errors."""
        registry = HookRegistry()
        manager = HookManager(registry)

        def rejecting_post_hook(context: HookContext) -> None:
            raise HookRejectionError(
                error_code="TRYING_TO_REJECT",
                message="POST hook should not reject",
                details={},
            )

        registry.register("rejecting", HookType.POST, rejecting_post_hook, "add")

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        errors = manager.execute_post_hooks(context)

        assert len(errors) == 1
        assert errors[0].hook_name == "rejecting"
        assert errors[0].error_code == "TRYING_TO_REJECT"

    def test_execute_post_hooks_continues_after_error(self):
        """Test that POST hooks continue executing after one fails."""
        registry = HookRegistry()
        manager = HookManager(registry)

        executed = []

        def hook1(context: HookContext) -> None:
            executed.append("hook1")

        def failing_hook(context: HookContext) -> None:
            executed.append("failing")
            raise ValueError("Failed")

        def hook3(context: HookContext) -> None:
            executed.append("hook3")

        registry.register("hook1", HookType.POST, hook1, "add", priority=10)
        registry.register("failing", HookType.POST, failing_hook, "add", priority=20)
        registry.register("hook3", HookType.POST, hook3, "add", priority=30)

        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        errors = manager.execute_post_hooks(context)

        # All hooks should execute
        assert executed == ["hook1", "failing", "hook3"]
        # Only one error
        assert len(errors) == 1
        assert errors[0].hook_name == "failing"


class TestHookContext(TestCase):
    """Tests for HookContext dataclass."""

    def test_hook_context_immutable(self):
        """Test that HookContext fields are frozen/immutable."""
        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        # Should not be able to modify frozen fields
        with self.assertRaises(Exception):  # FrozenInstanceError or AttributeError
            context.operation = "deduct"

        with self.assertRaises(Exception):
            context.amount = Decimal("200")

    def test_hook_context_metadata_mutable(self):
        """Test that metadata dict is mutable for inter-hook communication."""
        context = HookContext(
            operation="add",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="test",
            trans_type=None,
            iid=-100,
        )

        # Metadata should be mutable
        context.metadata["custom_key"] = "custom_value"
        assert context.metadata["custom_key"] == "custom_value"

    def test_hook_context_transfer_fields(self):
        """Test HookContext with transfer-specific fields."""
        context = HookContext(
            operation="transfer",
            user_id=1,
            point_type="cash",
            amount=Decimal("100"),
            remarks="transfer",
            trans_type=None,
            iid=-100,
            to_user_id=2,
            to_point_type="credit",
        )

        assert context.to_user_id == 2
        assert context.to_point_type == "credit"


class TestPostHookError(TestCase):
    """Tests for PostHookError dataclass."""

    def test_post_hook_error_creation(self):
        """Test creating a PostHookError."""
        exception = ValueError("Test error")
        error = PostHookError(
            hook_name="test_hook",
            error_code="TEST_ERROR",
            message="Test error message",
            details={"key": "value"},
            exception=exception,
        )

        assert error.hook_name == "test_hook"
        assert error.error_code == "TEST_ERROR"
        assert error.message == "Test error message"
        assert error.details["key"] == "value"
        assert error.exception is exception
