"""
Tests for audit metadata injection functionality.

Tests cover:
- MetadataInjectionHook execution and field injection
- Run ID generation and uniqueness
- Integration with Ingester
- YAML configuration options
- Field customization
"""

import pytest
import uuid
from datetime import datetime
from unittest.mock import patch

from atomsql import Database

from atomingest.pipeline.audit_metadata import (
    generate_run_id,
    MetadataInjectionHook,
    create_metadata_injection_hook,
)
from atomingest.pipeline.hooks import HookContext, HookType, HookRegistry
from atomingest.core.ingester import Ingester


class TestGenerateRunId:
    """Tests for run ID generation."""

    def test_generate_run_id_returns_valid_uuid(self):
        """Test that generate_run_id returns a valid UUID string."""
        run_id = generate_run_id()

        assert isinstance(run_id, str)
        # Should be parseable as UUID
        uuid_obj = uuid.UUID(run_id)
        assert str(uuid_obj) == run_id

    def test_generate_run_id_unique(self):
        """Test that consecutive calls generate unique IDs."""
        run_ids = [generate_run_id() for _ in range(100)]

        # All should be unique
        assert len(set(run_ids)) == 100


class TestMetadataInjectionHook:
    """Tests for MetadataInjectionHook class."""

    def test_hook_initialization_default_config(self):
        """Test hook initializes with default configuration."""
        hook = MetadataInjectionHook()

        assert hook.source_field_name == "source_file"
        assert hook.run_id_field_name == "run_id"
        assert hook.timestamp_field_name == "ingestion_timestamp"
        assert hook.include_timestamp is True
        assert hook.include_full_path is False

    def test_hook_initialization_custom_config(self):
        """Test hook initializes with custom configuration."""
        config = {
            "source_field_name": "file_source",
            "run_id_field_name": "ingestion_id",
            "timestamp_field_name": "processed_at",
            "include_timestamp": False,
            "include_full_path": True,
        }
        hook = MetadataInjectionHook(config=config)

        assert hook.source_field_name == "file_source"
        assert hook.run_id_field_name == "ingestion_id"
        assert hook.timestamp_field_name == "processed_at"
        assert hook.include_timestamp is False
        assert hook.include_full_path is True

    def test_execute_injects_metadata(self):
        """Test that execute adds metadata fields to row_data."""
        hook = MetadataInjectionHook()

        row_data = {"name": "John", "age": 30}
        context = HookContext(
            row_data=row_data,
            metadata={
                "source": "/path/to/data.csv",
                "run_id": "test-run-123",
            },
        )

        result = hook.execute(context)

        assert result.row_data["source_file"] == "data.csv"
        assert result.row_data["run_id"] == "test-run-123"
        assert "ingestion_timestamp" in result.row_data
        assert isinstance(result.row_data["ingestion_timestamp"], datetime)
        # Original fields should still be there
        assert result.row_data["name"] == "John"
        assert result.row_data["age"] == 30

    def test_execute_with_full_path(self):
        """Test that full path is included when configured."""
        hook = MetadataInjectionHook(config={"include_full_path": True})

        row_data = {"data": "test"}
        context = HookContext(
            row_data=row_data,
            metadata={
                "source": "/path/to/data.csv",
                "run_id": "test-run-123",
            },
        )

        result = hook.execute(context)

        # Should get full path
        assert result.row_data["source_file"] == "/path/to/data.csv"

    def test_execute_without_timestamp(self):
        """Test that timestamp is not included when disabled."""
        hook = MetadataInjectionHook(config={"include_timestamp": False})

        row_data = {"data": "test"}
        context = HookContext(
            row_data=row_data,
            metadata={
                "source": "/path/to/data.csv",
                "run_id": "test-run-123",
            },
        )

        result = hook.execute(context)

        assert "ingestion_timestamp" not in result.row_data

    def test_execute_with_missing_source(self):
        """Test that hook handles missing source gracefully."""
        hook = MetadataInjectionHook()

        row_data = {"data": "test"}
        context = HookContext(
            row_data=row_data,
            metadata={
                "run_id": "test-run-123",
            },
        )

        result = hook.execute(context)

        # Should not have source_file but should have run_id
        assert "source_file" not in result.row_data
        assert result.row_data["run_id"] == "test-run-123"

    def test_execute_with_missing_run_id(self):
        """Test that hook handles missing run_id gracefully."""
        hook = MetadataInjectionHook()

        row_data = {"data": "test"}
        context = HookContext(
            row_data=row_data,
            metadata={
                "source": "/path/to/data.csv",
            },
        )

        result = hook.execute(context)

        # Should have source_file but not run_id
        assert result.row_data["source_file"] == "data.csv"
        assert "run_id" not in result.row_data

    def test_execute_with_custom_field_names(self):
        """Test that custom field names are used."""
        hook = MetadataInjectionHook(
            config={
                "source_field_name": "file_origin",
                "run_id_field_name": "batch_id",
                "timestamp_field_name": "loaded_at",
            }
        )

        row_data = {"data": "test"}
        context = HookContext(
            row_data=row_data,
            metadata={
                "source": "/path/to/data.csv",
                "run_id": "test-run-123",
            },
        )

        result = hook.execute(context)

        assert result.row_data["file_origin"] == "data.csv"
        assert result.row_data["batch_id"] == "test-run-123"
        assert "loaded_at" in result.row_data

    def test_execute_with_no_row_data(self):
        """Test that hook handles None row_data gracefully."""
        hook = MetadataInjectionHook()

        context = HookContext(
            row_data=None,
            metadata={
                "source": "/path/to/data.csv",
                "run_id": "test-run-123",
            },
        )

        result = hook.execute(context)

        # Should return context unchanged
        assert result.row_data is None

    def test_create_metadata_injection_hook_factory(self):
        """Test factory function creates hook with config."""
        config = {"source_field_name": "origin"}
        hook = create_metadata_injection_hook(config)

        assert isinstance(hook, MetadataInjectionHook)
        assert hook.source_field_name == "origin"


class TestHookRegistryIntegration:
    """Tests for integration with HookRegistry."""

    def test_metadata_hook_registered_as_builtin(self):
        """Test that MetadataInjectionHook is available as built-in."""
        from atomingest.pipeline.hooks import HookRegistry

        assert "MetadataInjectionHook" in HookRegistry.BUILTIN_HOOKS
        assert (
            HookRegistry.BUILTIN_HOOKS["MetadataInjectionHook"] == MetadataInjectionHook
        )

    def test_hook_can_be_registered_and_executed(self):
        """Test that hook can be registered and executed through registry."""
        registry = HookRegistry()
        hook = MetadataInjectionHook()

        registry.register(HookType.PRE_SAVE, hook)

        row_data = {"data": "test"}
        context = HookContext(
            row_data=row_data,
            metadata={
                "source": "/path/to/data.csv",
                "run_id": "test-run-123",
            },
        )

        result = registry.execute_hooks(HookType.PRE_SAVE, context)

        assert result.row_data["source_file"] == "data.csv"
        assert result.row_data["run_id"] == "test-run-123"


class TestIngesterIntegration:
    """Tests for integration with Ingester."""

    @pytest.fixture
    def db(self):
        """Create an in-memory test database."""
        return Database("sqlite://:memory:")

    @pytest.fixture
    def temp_yaml_file(self, tmp_path):
        """Create a temporary YAML config file with audit metadata."""
        yaml_content = """
target_table: test_records
config:
  audit_metadata:
    enabled: true
    source_field_name: source_file
    run_id_field_name: run_id
    include_timestamp: true
schema:
  name:
    type: CharField
    max_length: 100
  description:
    type: CharField
    max_length: 255
  source_file:
    type: CharField
    max_length: 255
  run_id:
    type: CharField
    max_length: 36
  ingestion_timestamp: DateTimeField
normalization:
  file_type: csv
  skip_header: true
"""
        yaml_file = tmp_path / "config.yaml"
        yaml_file.write_text(yaml_content)
        return yaml_file

    @pytest.fixture
    def temp_csv_file(self, tmp_path):
        """Create a temporary CSV source file."""
        csv_content = """name,description
Alice,Engineer
Bob,Designer
Charlie,Manager
"""
        csv_file = tmp_path / "data.csv"
        csv_file.write_text(csv_content)
        return csv_file

    def test_ingester_adds_run_id_to_context(self, db, temp_yaml_file, temp_csv_file):
        """Test that Ingester generates and includes run_id in all contexts."""
        ingester = Ingester(db)

        # Track hook executions
        executed_contexts = []

        def capture_context(hook_type, context, fail_fast=True):
            if hasattr(context, "metadata"):
                executed_contexts.append(context)
            return context

        # Mock a hook to capture contexts
        with patch.object(
            HookRegistry, "execute_hooks", side_effect=capture_context
        ) as mock_execute:
            ingester.run(temp_yaml_file, temp_csv_file)

            # Check that run_id was in the contexts
            for call_args in mock_execute.call_args_list:
                if len(call_args[0]) > 1:  # Has context argument
                    context = call_args[0][1]
                    if hasattr(context, "metadata") and context.metadata:
                        if "run_id" in context.metadata:
                            # Verify it's a valid UUID
                            uuid.UUID(context.metadata["run_id"])

    def test_ingester_metadata_injection_enabled(
        self, db, temp_yaml_file, temp_csv_file
    ):
        """Test that metadata is injected when enabled in config."""
        ingester = Ingester(db)

        ingester.run(temp_yaml_file, temp_csv_file)

        # Query the data directly using SQL to verify metadata was injected
        tables = db.backend.get_tables()
        # The table name might have "model" appended by AtomSQL
        table_name = next((t for t in tables if t.startswith("test_records")), None)
        assert table_name is not None, f"test_records table not found in {tables}"

        # Get column info
        columns = db.backend.get_table_columns(table_name)
        # Columns are returned as tuples: (column_id, name, type, not_null, default, pk)
        column_names = [col[1] for col in columns]

        # Verify metadata columns exist
        assert "source_file" in column_names
        assert "run_id" in column_names
        assert "ingestion_timestamp" in column_names

        # Query the data using raw SQL
        cursor = db.backend.execute(f"SELECT * FROM {table_name}", ())
        records = cursor.fetchall()

        assert len(records) == 3
        # Check that metadata fields are populated
        for record in records:
            # source_file should be the filename
            source_file_idx = column_names.index("source_file")
            assert record[source_file_idx] == "data.csv"

            # run_id should be a valid UUID
            run_id_idx = column_names.index("run_id")
            uuid.UUID(record[run_id_idx])

            # All records should have the same run_id (same ingestion run)
            if records.index(record) > 0:
                assert record[run_id_idx] == records[0][run_id_idx]

            # ingestion_timestamp should exist
            timestamp_idx = column_names.index("ingestion_timestamp")
            assert record[timestamp_idx] is not None

    def test_multiple_ingestion_runs_different_run_ids(
        self, db, temp_yaml_file, temp_csv_file
    ):
        """Test that different ingestion runs get different run IDs."""
        ingester = Ingester(db)

        # First ingestion
        ingester.run(temp_yaml_file, temp_csv_file)

        # Get run_id from first ingestion
        from atomingest.core.loader import SchemaLoader

        loader = SchemaLoader()
        schema = loader.load_from_yaml(str(temp_yaml_file))
        ModelClass = loader.create_model(schema)
        ModelClass._table_name = "test_records"
        db.register(ModelClass)

        records1 = list(ModelClass.objects())
        run_id1 = records1[0].run_id if records1 else None

        # Second ingestion (simulate by creating new CSV)
        csv_content2 = """name,description
Dave,Analyst
Eve,Developer
"""
        csv_file2 = temp_csv_file.parent / "data2.csv"
        csv_file2.write_text(csv_content2)

        ingester.run(temp_yaml_file, csv_file2)

        records2 = list(ModelClass.objects())
        # Should have records from both ingestions
        assert len(records2) == 5

        # Get run_ids
        run_ids = set(r.run_id for r in records2)

        # Should have 2 different run_ids
        assert len(run_ids) == 2
        assert run_id1 in run_ids


class TestYAMLConfiguration:
    """Tests for YAML configuration of audit metadata."""

    def test_audit_metadata_disabled(self, tmp_path):
        """Test that metadata injection can be disabled."""
        yaml_content = """
target_table: test_records
config:
  audit_metadata:
    enabled: false
schema:
  name:
    type: CharField
    max_length: 100
"""
        yaml_file = tmp_path / "config.yaml"
        yaml_file.write_text(yaml_content)

        csv_content = "name\\nTest"
        csv_file = tmp_path / "data.csv"
        csv_file.write_text(csv_content)

        db = Database("sqlite://:memory:")
        ingester = Ingester(db)

        # This should not register the metadata hook
        # (Testing would require checking hook registry state)
        # For now, just ensure it doesn't crash
        ingester.run(yaml_file, csv_file)

    def test_audit_metadata_custom_fields(self, tmp_path):
        """Test custom field names in configuration."""
        yaml_content = """
target_table: test_records
config:
  audit_metadata:
    enabled: true
    source_field_name: data_source
    run_id_field_name: batch_id
    timestamp_field_name: processed_at
    include_full_path: true
schema:
  name:
    type: CharField
    max_length: 100
  data_source:
    type: CharField
    max_length: 255
  batch_id:
    type: CharField
    max_length: 36
  processed_at: DateTimeField
normalization:
  file_type: csv
  skip_header: false
"""
        yaml_file = tmp_path / "config.yaml"
        yaml_file.write_text(yaml_content)

        csv_content = "Test"
        csv_file = tmp_path / "data.csv"
        csv_file.write_text(csv_content)

        db = Database("sqlite://:memory:")
        ingester = Ingester(db)

        ingester.run(yaml_file, csv_file)

        # Verify custom fields were used
        from atomingest.core.loader import SchemaLoader

        loader = SchemaLoader()
        schema = loader.load_from_yaml(str(yaml_file))
        ModelClass = loader.create_model(schema)
        ModelClass._table_name = "test_records"
        db.register(ModelClass)

        records = list(ModelClass.objects())
        if records:
            record = records[0]
            assert hasattr(record, "data_source")
            assert hasattr(record, "batch_id")
            assert hasattr(record, "processed_at")
            # Full path should be included
            assert "/" in record.data_source or "\\\\" in record.data_source
