"""
Tests for the pipeline hooks module.
"""

import pytest
from typing import Any, Dict
from unittest.mock import MagicMock, patch
from datetime import datetime

from atomsql import Database

from atomingest.pipeline.hooks import (
    Hook,
    HookType,
    HookContext,
    HookRegistry,
    HookConfig,
    HookExecutionError,
    FunctionHook,
    PreSaveHook,
    PostSaveHook,
    TransformHook,
    OnErrorHook,
    OnCompleteHook,
    DeadLetterQueueHook,
    DLQFailureModel,
    parse_hooks_config,
    create_registry_from_config,
)


class TestHookContext:
    """Tests for HookContext dataclass."""

    def test_default_values(self):
        """Test HookContext has sensible defaults."""
        context = HookContext(row_data={"id": 1})
        assert context.row_data == {"id": 1}
        assert context.instance is None
        assert context.metadata == {}
        assert context.error is None
        assert context.skip_row is False
        assert context.abort is False

    def test_with_all_values(self):
        """Test HookContext with all values set."""
        error = ValueError("test error")
        instance = MagicMock()
        context = HookContext(
            row_data={"id": 1, "name": "test"},
            instance=instance,
            metadata={"source": "test.csv"},
            error=error,
            skip_row=True,
            abort=False,
        )
        assert context.row_data == {"id": 1, "name": "test"}
        assert context.instance is instance
        assert context.metadata == {"source": "test.csv"}
        assert context.error is error
        assert context.skip_row is True
        assert context.abort is False


class TestHookType:
    """Tests for HookType enum."""

    def test_all_hook_types_exist(self):
        """Test all expected hook types exist."""
        assert HookType.PRE_NORMALIZATION is not None
        assert HookType.POST_NORMALIZATION is not None
        assert HookType.PRE_SAVE is not None
        assert HookType.POST_SAVE is not None
        assert HookType.ON_ERROR is not None
        assert HookType.ON_COMPLETE is not None

    def test_hook_type_values(self):
        """Test hook type values for YAML mapping."""
        assert HookType.PRE_NORMALIZATION.value == "pre_normalization"
        assert HookType.POST_NORMALIZATION.value == "post_normalization"
        assert HookType.PRE_SAVE.value == "pre_save"
        assert HookType.POST_SAVE.value == "post_save"
        assert HookType.ON_ERROR.value == "on_error"
        assert HookType.ON_COMPLETE.value == "on_complete"


class TestFunctionHook:
    """Tests for FunctionHook class."""

    def test_function_hook_executes(self):
        """Test FunctionHook wraps and executes a function."""

        def my_hook(context: HookContext) -> HookContext:
            context.row_data["modified"] = True
            return context

        hook = FunctionHook(my_hook)
        context = HookContext(row_data={"id": 1})
        result = hook.execute(context)

        assert result.row_data["modified"] is True

    def test_function_hook_with_config(self):
        """Test FunctionHook with config stored on hook."""

        def configurable_hook(context: HookContext) -> HookContext:
            # Access config through context metadata (set by caller)
            return context

        hook = FunctionHook(
            configurable_hook, name="test_hook", config={"uppercase": True}
        )
        assert hook.config == {"uppercase": True}

    def test_function_hook_name(self):
        """Test FunctionHook uses function name."""

        def my_custom_hook(ctx):
            return ctx

        hook = FunctionHook(my_custom_hook)
        assert hook.name == "my_custom_hook"


class TestBuiltInHooks:
    """Tests for built-in hook classes."""

    def test_pre_save_hook_sets_type(self):
        """Test PreSaveHook has correct hook type."""

        class MyPreSave(PreSaveHook):
            def execute(self, context: HookContext) -> HookContext:
                return context

        hook = MyPreSave()
        assert hook.hook_type == HookType.PRE_SAVE

    def test_post_save_hook_sets_type(self):
        """Test PostSaveHook has correct hook type."""

        class MyPostSave(PostSaveHook):
            def execute(self, context: HookContext) -> HookContext:
                return context

        hook = MyPostSave()
        assert hook.hook_type == HookType.POST_SAVE

    def test_transform_hook_modifies_data(self):
        """Test TransformHook can modify row data."""

        class UppercaseHook(TransformHook):
            def transform(
                self, row_data: Dict[str, Any], context: HookContext
            ) -> Dict[str, Any]:
                for key, value in row_data.items():
                    if isinstance(value, str):
                        row_data[key] = value.upper()
                return row_data

        hook = UppercaseHook()
        context = HookContext(row_data={"name": "john", "city": "london"})
        result = hook.execute(context)

        assert result.row_data["name"] == "JOHN"
        assert result.row_data["city"] == "LONDON"

    def test_on_error_hook_receives_error(self):
        """Test OnErrorHook receives error in context."""
        error_received = []

        class ErrorLoggerHook(OnErrorHook):
            def execute(self, context: HookContext) -> HookContext:
                error_received.append(context.error)
                return context

        hook = ErrorLoggerHook()
        error = ValueError("test error")
        context = HookContext(row_data={}, error=error)
        hook.execute(context)

        assert len(error_received) == 1
        assert error_received[0] is error

    def test_on_complete_hook_receives_stats(self):
        """Test OnCompleteHook receives stats in metadata."""
        stats_received = []

        class StatsLoggerHook(OnCompleteHook):
            def execute(self, context: HookContext) -> HookContext:
                stats_received.append(context.metadata.get("stats"))
                return context

        hook = StatsLoggerHook()
        context = HookContext(
            row_data={}, metadata={"stats": {"processed": 10, "failed": 2}}
        )
        hook.execute(context)

        assert stats_received[0] == {"processed": 10, "failed": 2}


class TestHookRegistry:
    """Tests for HookRegistry class."""

    def test_register_hook(self):
        """Test registering a hook."""
        registry = HookRegistry()
        hook = FunctionHook(lambda ctx: ctx)

        registry.register(HookType.PRE_SAVE, hook)

        assert len(registry.get_hooks(HookType.PRE_SAVE)) == 1

    def test_register_multiple_hooks_same_type(self):
        """Test registering multiple hooks of same type."""
        registry = HookRegistry()
        hook1 = FunctionHook(lambda ctx: ctx, name="hook1")
        hook2 = FunctionHook(lambda ctx: ctx, name="hook2")

        registry.register(HookType.PRE_SAVE, hook1, order=2)
        registry.register(HookType.PRE_SAVE, hook2, order=1)

        assert len(registry.get_hooks(HookType.PRE_SAVE)) == 2

    def test_execute_hooks_in_order(self):
        """Test hooks execute in order."""
        execution_order = []

        def make_hook(name):
            def hook_fn(ctx):
                execution_order.append(name)
                return ctx

            return FunctionHook(hook_fn, name=name)

        registry = HookRegistry()
        registry.register(HookType.PRE_SAVE, make_hook("third"), order=3)
        registry.register(HookType.PRE_SAVE, make_hook("first"), order=1)
        registry.register(HookType.PRE_SAVE, make_hook("second"), order=2)

        context = HookContext(row_data={})
        registry.execute_hooks(HookType.PRE_SAVE, context)

        assert execution_order == ["first", "second", "third"]

    def test_execute_hooks_chains_context(self):
        """Test hooks receive modified context from previous hooks."""

        def hook1(ctx):
            ctx.row_data["step1"] = True
            return ctx

        def hook2(ctx):
            ctx.row_data["step2"] = ctx.row_data.get("step1", False)
            return ctx

        registry = HookRegistry()
        registry.register(HookType.PRE_SAVE, FunctionHook(hook1), order=1)
        registry.register(HookType.PRE_SAVE, FunctionHook(hook2), order=2)

        context = HookContext(row_data={})
        result = registry.execute_hooks(HookType.PRE_SAVE, context)

        assert result.row_data["step1"] is True
        assert result.row_data["step2"] is True

    def test_execute_hooks_no_hooks_registered(self):
        """Test executing when no hooks registered returns context unchanged."""
        registry = HookRegistry()
        context = HookContext(row_data={"original": True})
        result = registry.execute_hooks(HookType.PRE_SAVE, context)

        assert result.row_data == {"original": True}

    def test_get_hooks_returns_sorted_list(self):
        """Test get_hooks returns hooks in order."""
        registry = HookRegistry()
        hook1 = FunctionHook(lambda ctx: ctx, name="hook1")
        hook2 = FunctionHook(lambda ctx: ctx, name="hook2")

        registry.register(HookType.PRE_SAVE, hook1, order=2)
        registry.register(HookType.PRE_SAVE, hook2, order=1)

        hooks = registry.get_hooks(HookType.PRE_SAVE)
        assert hooks[0].name == "hook2"  # order=1
        assert hooks[1].name == "hook1"  # order=2

    def test_clear_hooks_by_type(self):
        """Test clearing hooks by type."""
        registry = HookRegistry()
        registry.register(HookType.PRE_SAVE, FunctionHook(lambda ctx: ctx))
        registry.register(HookType.POST_SAVE, FunctionHook(lambda ctx: ctx))

        registry.clear(HookType.PRE_SAVE)

        assert len(registry.get_hooks(HookType.PRE_SAVE)) == 0
        assert len(registry.get_hooks(HookType.POST_SAVE)) == 1

    def test_clear_all_hooks(self):
        """Test clearing all hooks."""
        registry = HookRegistry()
        registry.register(HookType.PRE_SAVE, FunctionHook(lambda ctx: ctx))
        registry.register(HookType.POST_SAVE, FunctionHook(lambda ctx: ctx))

        registry.clear()

        assert len(registry.get_hooks(HookType.PRE_SAVE)) == 0
        assert len(registry.get_hooks(HookType.POST_SAVE)) == 0

    def test_hook_execution_error_handling(self):
        """Test hook execution error is wrapped in HookExecutionError."""

        def failing_hook(ctx):
            raise ValueError("Hook failed")

        registry = HookRegistry()
        registry.register(HookType.PRE_SAVE, FunctionHook(failing_hook))

        context = HookContext(row_data={})

        with pytest.raises(HookExecutionError) as exc_info:
            registry.execute_hooks(HookType.PRE_SAVE, context)

        assert "Hook failed" in str(exc_info.value)


class TestHookConfig:
    """Tests for HookConfig dataclass."""

    def test_hook_config_with_function(self):
        """Test HookConfig with function path."""
        config = HookConfig(
            hook_type=HookType.PRE_SAVE, function_path="myapp.hooks.transform_name"
        )
        assert config.hook_type == HookType.PRE_SAVE
        assert config.function_path == "myapp.hooks.transform_name"
        assert config.module_path is None

    def test_hook_config_with_module(self):
        """Test HookConfig with module path."""
        config = HookConfig(
            hook_type=HookType.POST_SAVE, module_path="myapp.hooks.AuditHook"
        )
        assert config.hook_type == HookType.POST_SAVE
        assert config.module_path == "myapp.hooks.AuditHook"
        assert config.function_path is None

    def test_hook_config_with_extra_config(self):
        """Test HookConfig with additional configuration."""
        config = HookConfig(
            hook_type=HookType.PRE_SAVE,
            function_path="myapp.hooks.validate",
            config={"strict": True, "fields": ["name", "email"]},
        )
        assert config.config == {"strict": True, "fields": ["name", "email"]}

    def test_hook_config_requires_path(self):
        """Test HookConfig requires either module_path or function_path."""
        with pytest.raises(
            ValueError,
            match="Either 'module_path' or 'function_path' must be specified",
        ):
            HookConfig(hook_type=HookType.PRE_SAVE)

    def test_hook_config_order_defaults(self):
        """Test HookConfig order defaults to 0."""
        config = HookConfig(
            hook_type=HookType.PRE_SAVE, function_path="myapp.hooks.validate"
        )
        assert config.order == 0

    def test_hook_config_custom_order(self):
        """Test HookConfig with custom order."""
        config = HookConfig(
            hook_type=HookType.PRE_SAVE, function_path="myapp.hooks.validate", order=5
        )
        assert config.order == 5


class TestParseHooksConfig:
    """Tests for parse_hooks_config function."""

    def test_parse_empty_config(self):
        """Test parsing empty hooks config."""
        result = parse_hooks_config({})
        assert result == []

    def test_parse_single_hook_type(self):
        """Test parsing single hook type."""
        yaml_config = {"pre_save": [{"function": "myapp.hooks.transform"}]}
        result = parse_hooks_config(yaml_config)

        assert len(result) == 1
        assert result[0].hook_type == HookType.PRE_SAVE
        assert result[0].function_path == "myapp.hooks.transform"

    def test_parse_multiple_hook_types(self):
        """Test parsing multiple hook types."""
        yaml_config = {
            "pre_save": [{"function": "myapp.hooks.pre_transform"}],
            "post_save": [{"function": "myapp.hooks.post_log"}],
            "on_error": [{"module": "myapp.hooks.ErrorHandler"}],
        }
        result = parse_hooks_config(yaml_config)

        assert len(result) == 3
        hook_types = {h.hook_type for h in result}
        assert hook_types == {HookType.PRE_SAVE, HookType.POST_SAVE, HookType.ON_ERROR}

    def test_parse_hooks_with_order(self):
        """Test parsing hooks with order specified."""
        yaml_config = {
            "pre_save": [
                {"function": "myapp.hooks.second", "order": 2},
                {"function": "myapp.hooks.first", "order": 1},
            ]
        }
        result = parse_hooks_config(yaml_config)

        assert len(result) == 2
        assert result[0].order == 2
        assert result[1].order == 1

    def test_parse_hooks_with_config(self):
        """Test parsing hooks with additional config."""
        yaml_config = {
            "pre_save": [
                {
                    "function": "myapp.hooks.validate",
                    "config": {"strict": True, "fields": ["name", "email"]},
                }
            ]
        }
        result = parse_hooks_config(yaml_config)

        assert len(result) == 1
        assert result[0].config == {"strict": True, "fields": ["name", "email"]}

    def test_parse_hooks_with_module_class(self):
        """Test parsing hooks with module/class path."""
        yaml_config = {"post_save": [{"module": "myapp.hooks.AuditLogger"}]}
        result = parse_hooks_config(yaml_config)

        assert len(result) == 1
        assert result[0].module_path == "myapp.hooks.AuditLogger"
        assert result[0].function_path is None


class TestCreateRegistryFromConfig:
    """Tests for create_registry_from_config function."""

    def test_create_registry_with_function_hook(self):
        """Test creating registry with function-based hook."""

        # Create a simple test function
        def test_transform(context: HookContext) -> HookContext:
            context.row_data["transformed"] = True
            return context

        # Mock the import
        with patch("atomingest.pipeline.hooks.importlib.import_module") as mock_import:
            mock_module = MagicMock()
            mock_module.test_transform = test_transform
            mock_import.return_value = mock_module

            # Pass dict format (YAML-like)
            yaml_config = {"pre_save": [{"function": "myapp.hooks.test_transform"}]}

            registry = create_registry_from_config(yaml_config)

            # Verify hook was registered
            hooks = registry.get_hooks(HookType.PRE_SAVE)
            assert len(hooks) == 1

    def test_create_registry_with_class_hook(self):
        """Test creating registry with class-based hook."""

        # Create a test hook class
        class TestHook(Hook):
            hook_type = HookType.POST_SAVE

            def execute(self, context: HookContext) -> HookContext:
                return context

        with patch("atomingest.pipeline.hooks.importlib.import_module") as mock_import:
            mock_module = MagicMock()
            mock_module.TestHook = TestHook
            mock_import.return_value = mock_module

            yaml_config = {"post_save": [{"module": "myapp.hooks.TestHook"}]}

            registry = create_registry_from_config(yaml_config)

            hooks = registry.get_hooks(HookType.POST_SAVE)
            assert len(hooks) == 1

    def test_create_registry_empty_config(self):
        """Test creating registry with empty config."""
        registry = create_registry_from_config({})

        # Should return empty registry
        for hook_type in HookType:
            assert len(registry.get_hooks(hook_type)) == 0


class TestHookSkipAndAbort:
    """Tests for skip_row and abort functionality."""

    def test_skip_row_flag(self):
        """Test skip_row flag in context."""

        def skip_hook(ctx):
            if ctx.row_data.get("skip"):
                ctx.skip_row = True
            return ctx

        registry = HookRegistry()
        registry.register(HookType.PRE_SAVE, FunctionHook(skip_hook))

        context = HookContext(row_data={"skip": True})
        result = registry.execute_hooks(HookType.PRE_SAVE, context)

        assert result.skip_row is True

    def test_abort_flag(self):
        """Test abort flag in context."""

        def abort_hook(ctx):
            if ctx.row_data.get("critical_error"):
                ctx.abort = True
            return ctx

        registry = HookRegistry()
        registry.register(HookType.PRE_SAVE, FunctionHook(abort_hook))

        context = HookContext(row_data={"critical_error": True})
        result = registry.execute_hooks(HookType.PRE_SAVE, context)

        assert result.abort is True


class TestHookIntegration:
    """Integration tests for hooks in the pipeline."""

    def test_full_hook_lifecycle(self):
        """Test a complete hook lifecycle through registry."""
        execution_log = []

        def pre_save_hook(ctx):
            execution_log.append("pre_save")
            ctx.row_data["pre_processed"] = True
            return ctx

        def post_save_hook(ctx):
            execution_log.append("post_save")
            return ctx

        def on_complete_hook(ctx):
            execution_log.append("on_complete")
            return ctx

        registry = HookRegistry()
        registry.register(HookType.PRE_SAVE, FunctionHook(pre_save_hook))
        registry.register(HookType.POST_SAVE, FunctionHook(post_save_hook))
        registry.register(HookType.ON_COMPLETE, FunctionHook(on_complete_hook))

        # Simulate pipeline execution
        context = HookContext(row_data={"id": 1})

        context = registry.execute_hooks(HookType.PRE_SAVE, context)
        assert context.row_data["pre_processed"] is True

        # Simulate save...
        context.instance = MagicMock()

        context = registry.execute_hooks(HookType.POST_SAVE, context)

        complete_context = HookContext(row_data={}, metadata={"stats": {}})
        registry.execute_hooks(HookType.ON_COMPLETE, complete_context)

        assert execution_log == ["pre_save", "post_save", "on_complete"]


class TestDeadLetterQueueHook:
    """Tests for DeadLetterQueueHook class."""

    @pytest.fixture
    def db(self):
        """Create an in-memory database for testing."""
        database = Database("sqlite:///:memory:")
        yield database
        database.close()

    @pytest.fixture
    def dlq_hook(self):
        """Create a DeadLetterQueueHook instance."""
        return DeadLetterQueueHook(name="test_dlq")

    def test_dlq_hook_is_on_error_hook(self, dlq_hook):
        """Test that DeadLetterQueueHook is an OnErrorHook."""
        assert isinstance(dlq_hook, OnErrorHook)
        assert dlq_hook.hook_type == HookType.ON_ERROR

    def test_dlq_hook_saves_failed_row_to_database(self, db, dlq_hook):
        """Test that DLQ hook saves failed row data to the database."""
        # Prepare context with error
        error = ValueError("Invalid data format")
        context = HookContext(
            row_data={"id": 1, "name": "John", "email": "invalid-email"},
            error=error,
            metadata={
                "db": db,
                "table": "users",
                "source": "test_file.csv",
            },
        )

        # Execute the hook
        result = dlq_hook.execute(context)

        # Verify context is returned
        assert result is context

        # Verify failure was saved to DLQ
        db.register(DLQFailureModel)
        failures = list(DLQFailureModel.objects())
        assert len(failures) == 1

        failure = failures[0]
        assert failure.row_data == {"id": 1, "name": "John", "email": "invalid-email"}
        assert failure.error_message == "Invalid data format"
        assert failure.target_table == "users"
        assert failure.source == "test_file.csv"
        assert isinstance(failure.timestamp, datetime)

    def test_dlq_hook_handles_missing_db_gracefully(self, dlq_hook, caplog):
        """Test that DLQ hook handles missing database connection gracefully."""
        # Context without database
        context = HookContext(
            row_data={"id": 1, "name": "John"},
            error=ValueError("Test error"),
            metadata={},  # No db key
        )

        # Execute hook
        with caplog.at_level("WARNING"):
            result = dlq_hook.execute(context)

        # Verify context is returned unchanged
        assert result is context

        # Verify warning was logged about missing db
        assert "no 'db' in context.metadata" in caplog.text

    def test_dlq_hook_handles_save_failure_gracefully(self, db, dlq_hook, caplog):
        """Test that DLQ hook handles save failures without raising exceptions."""
        # Mock the save method to raise an exception
        with patch.object(
            DLQFailureModel, "save", side_effect=Exception("Database error")
        ):
            context = HookContext(
                row_data={"id": 1, "name": "John"},
                error=ValueError("Original error"),
                metadata={"db": db, "table": "users", "source": "test.csv"},
            )

            # Execute hook
            with caplog.at_level("CRITICAL"):
                dlq_hook.execute(context)

            # Verify critical error was logged
            assert "CRITICAL: Failed to save to DLQ" in caplog.text
            assert "Database error" in caplog.text

    def test_dlq_hook_with_different_error_types(self, db, dlq_hook):
        """Test DLQ hook handles different error types correctly."""
        error_types = [
            ValueError("Value error"),
            KeyError("missing_key"),
            TypeError("Type mismatch"),
            RuntimeError("Runtime issue"),
        ]

        for error in error_types:
            context = HookContext(
                row_data={"test": "data"},
                error=error,
                metadata={"db": db, "table": "test_table", "source": "test.csv"},
            )

            dlq_hook.execute(context)

        # Verify all errors were saved
        db.register(DLQFailureModel)
        failures = list(DLQFailureModel.objects())
        assert len(failures) == len(error_types)

        # Verify error messages
        error_messages = [f.error_message for f in failures]
        assert "Value error" in error_messages
        assert (
            "'missing_key'" in error_messages
        )  # KeyError converts to string with quotes
        assert "Type mismatch" in error_messages
        assert "Runtime issue" in error_messages

    def test_dlq_hook_with_unknown_metadata_values(self, db, dlq_hook):
        """Test DLQ hook uses 'unknown' defaults when metadata is missing."""
        context = HookContext(
            row_data={"id": 1, "name": "Test"},
            error=ValueError("Error"),
            metadata={"db": db},  # Missing table and source
        )

        dlq_hook.execute(context)

        # Verify defaults were used
        db.register(DLQFailureModel)
        failures = list(DLQFailureModel.objects())
        assert len(failures) == 1

        failure = failures[0]
        assert failure.target_table == "unknown"
        assert failure.source == "unknown"

    def test_dlq_hook_preserves_complex_row_data(self, db, dlq_hook):
        """Test that DLQ hook preserves complex row data structures."""
        complex_row = {
            "id": 123,
            "name": "Test User",
            "nested": {"key1": "value1", "key2": [1, 2, 3]},
            "list_field": ["a", "b", "c"],
            "numeric": 42.5,
            "boolean": True,
            "null_value": None,
        }

        context = HookContext(
            row_data=complex_row,
            error=ValueError("Complex data error"),
            metadata={"db": db, "table": "complex_table", "source": "test.csv"},
        )

        dlq_hook.execute(context)

        # Verify complex data was preserved
        db.register(DLQFailureModel)
        failures = list(DLQFailureModel.objects())
        assert len(failures) == 1

        failure = failures[0]
        assert failure.row_data == complex_row
        assert failure.row_data["nested"]["key2"] == [1, 2, 3]
        assert failure.row_data["list_field"] == ["a", "b", "c"]

    def test_dlq_hook_logs_warning_on_success(self, db, dlq_hook, caplog):
        """Test that DLQ hook logs a warning message when row is successfully routed to DLQ."""
        context = HookContext(
            row_data={"id": 1, "name": "Test"},
            error=ValueError("Test error message"),
            metadata={"db": db, "table": "test_table", "source": "test.csv"},
        )

        with caplog.at_level("WARNING"):
            dlq_hook.execute(context)

        # Verify warning was logged
        assert "Row routed to DLQ" in caplog.text
        assert "Test error message" in caplog.text

    def test_dlq_hook_multiple_failures_same_pipeline(self, db, dlq_hook):
        """Test DLQ hook can handle multiple failures in the same pipeline run."""
        num_failures = 5

        for i in range(num_failures):
            context = HookContext(
                row_data={"id": i, "name": f"User {i}"},
                error=ValueError(f"Error {i}"),
                metadata={"db": db, "table": "users", "source": "batch.csv"},
            )
            dlq_hook.execute(context)

        # Verify all failures were saved
        db.register(DLQFailureModel)
        failures = list(DLQFailureModel.objects())
        assert len(failures) == num_failures

        # Verify each has unique data
        row_ids = [f.row_data["id"] for f in failures]
        assert sorted(row_ids) == list(range(num_failures))

    def test_dlq_hook_in_hook_registry(self, db):
        """Test that DeadLetterQueueHook can be registered and executed via HookRegistry."""
        registry = HookRegistry()
        dlq_hook = DeadLetterQueueHook(name="dlq_handler")

        registry.register(HookType.ON_ERROR, dlq_hook)

        # Execute via registry
        context = HookContext(
            row_data={"id": 1, "data": "test"},
            error=ValueError("Registry test error"),
            metadata={"db": db, "table": "test", "source": "test.csv"},
        )

        registry.execute_hooks(HookType.ON_ERROR, context)

        # Verify hook was executed
        db.register(DLQFailureModel)
        failures = list(DLQFailureModel.objects())
        assert len(failures) == 1
        assert failures[0].error_message == "Registry test error"

    def test_dlq_model_fields_are_correct(self):
        """Test that DLQFailureModel has the expected field definitions."""
        from atomsql.orm import fields as atomsql_fields

        # Verify field types
        assert hasattr(DLQFailureModel, "row_data")
        assert hasattr(DLQFailureModel, "error_message")
        assert hasattr(DLQFailureModel, "target_table")
        assert hasattr(DLQFailureModel, "source")
        assert hasattr(DLQFailureModel, "timestamp")

        # Get field definitions from model's _fields
        model_fields = DLQFailureModel._fields

        # Verify row_data is JSONField and not nullable
        assert isinstance(model_fields["row_data"], atomsql_fields.JSONField)
        assert not model_fields["row_data"].nullable

        # Verify error_message is TextField and not nullable
        assert isinstance(model_fields["error_message"], atomsql_fields.TextField)
        assert not model_fields["error_message"].nullable

        # Verify target_table is CharField
        assert isinstance(model_fields["target_table"], atomsql_fields.CharField)
        assert model_fields["target_table"].max_length == 255
        assert not model_fields["target_table"].nullable

        # Verify source is CharField and nullable
        assert isinstance(model_fields["source"], atomsql_fields.CharField)
        assert model_fields["source"].max_length == 255
        assert model_fields["source"].nullable

        # Verify timestamp is DateTimeField
        assert isinstance(model_fields["timestamp"], atomsql_fields.DateTimeField)

    def test_dlq_hook_builtin_hooks_registry(self):
        """Test that DeadLetterQueueHook is registered in BUILTIN_HOOKS."""
        assert "DeadLetterQueueHook" in HookRegistry.BUILTIN_HOOKS
        assert HookRegistry.BUILTIN_HOOKS["DeadLetterQueueHook"] is DeadLetterQueueHook

    def test_dlq_hook_with_empty_row_data(self, db, dlq_hook):
        """Test DLQ hook handles empty row data."""
        context = HookContext(
            row_data={},
            error=ValueError("Empty row error"),
            metadata={"db": db, "table": "test", "source": "test.csv"},
        )

        dlq_hook.execute(context)

        # Verify empty dict was saved
        db.register(DLQFailureModel)
        failures = list(DLQFailureModel.objects())
        assert len(failures) == 1
        assert failures[0].row_data == {}

    def test_dlq_hook_timestamp_is_recent(self, db, dlq_hook):
        """Test that DLQ hook records a recent timestamp."""
        before = datetime.now()

        context = HookContext(
            row_data={"id": 1},
            error=ValueError("Test"),
            metadata={"db": db, "table": "test", "source": "test.csv"},
        )

        dlq_hook.execute(context)

        after = datetime.now()

        # Verify timestamp is between before and after
        db.register(DLQFailureModel)
        failures = list(DLQFailureModel.objects())
        assert len(failures) == 1

        timestamp = failures[0].timestamp
        assert before <= timestamp <= after
