"""
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 tempfile
import uuid
from pathlib import Path
from datetime import datetime
from unittest.mock import Mock, patch

from atomsql import Database
from atomsql.orm import fields, Model

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(ctx):
            executed_contexts.append(ctx)
            return ctx
        
        # Mock a hook to capture contexts
        with patch.object(HookRegistry, 'execute_hooks', side_effect=capture_context) as mock_execute:
            stats = 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)
        
        stats = 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
        stats1 = 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)
        
        stats2 = 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
        stats = ingester.run(yaml_file, csv_file)
        assert stats.processed >= 0

    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)
        
        stats = 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
