"""
Unit tests for ETL validation module (validation.py).

Tests:
- ETLValidator class with different on_error modes
- Schema and contract resolution
- DLQ integration
"""

import asyncio
import os
import tempfile
from pathlib import Path

import pytest
import yaml

from pycharter.etl_generator.config_models import (
    DLQBackend,
    DLQConfig,
    ExtractValidationConfig,
    LoadValidationConfig,
    OnErrorAction,
)
from pycharter.etl_generator.validation import (
    ETLValidationError,
    ETLValidator,
    create_dlq,
    create_etl_validator,
    resolve_contract,
    resolve_schema,
)


class TestETLValidator:
    """Test ETLValidator class."""

    @pytest.fixture
    def simple_schema(self):
        """Simple schema for testing."""
        return {
            "version": "1.0.0",
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
            },
            "required": ["name"],
        }

    @pytest.fixture
    def validator_skip(self, simple_schema):
        """Validator with on_error=skip."""
        return ETLValidator(
            schema_or_contract=simple_schema,
            on_error=OnErrorAction.SKIP,
            pipeline_name="test_pipeline",
            stage="extract",
        )

    @pytest.fixture
    def validator_fail(self, simple_schema):
        """Validator with on_error=fail."""
        return ETLValidator(
            schema_or_contract=simple_schema,
            on_error=OnErrorAction.FAIL,
            pipeline_name="test_pipeline",
            stage="extract",
        )

    @pytest.fixture
    def validator_warn(self, simple_schema):
        """Validator with on_error=warn."""
        return ETLValidator(
            schema_or_contract=simple_schema,
            on_error=OnErrorAction.WARN,
            pipeline_name="test_pipeline",
            stage="extract",
        )

    @pytest.fixture
    def validator_quarantine(self, simple_schema):
        """Validator with on_error=quarantine."""
        return ETLValidator(
            schema_or_contract=simple_schema,
            on_error=OnErrorAction.QUARANTINE,
            pipeline_name="test_pipeline",
            stage="extract",
        )

    @pytest.mark.asyncio
    async def test_validate_all_valid(self, validator_skip):
        """Test validation with all valid records."""
        data = [
            {"name": "Alice", "age": 30},
            {"name": "Bob", "age": 25},
        ]
        valid, quarantined, error_count = await validator_skip.validate(data)
        assert len(valid) == 2
        assert len(quarantined) == 0
        assert error_count == 0

    @pytest.mark.asyncio
    async def test_validate_empty_data(self, validator_skip):
        """Test validation with empty data."""
        valid, quarantined, error_count = await validator_skip.validate([])
        assert len(valid) == 0
        assert len(quarantined) == 0
        assert error_count == 0

    @pytest.mark.asyncio
    async def test_validate_skip_invalid(self, validator_skip):
        """Test validation with skip mode - invalid records are skipped."""
        data = [
            {"name": "Alice", "age": 30},
            {"age": 25},  # Missing required 'name'
            {"name": "Charlie", "age": 35},
        ]
        valid, quarantined, error_count = await validator_skip.validate(data)
        assert len(valid) == 2  # Alice and Charlie
        assert len(quarantined) == 0  # Skip mode doesn't quarantine
        assert error_count == 1

    @pytest.mark.asyncio
    async def test_validate_fail_on_error(self, validator_fail):
        """Test validation with fail mode - raises on invalid."""
        data = [
            {"name": "Alice", "age": 30},
            {"age": 25},  # Missing required 'name'
        ]
        with pytest.raises(ETLValidationError) as exc_info:
            await validator_fail.validate(data)
        assert exc_info.value.invalid_count == 1

    @pytest.mark.asyncio
    async def test_validate_warn_mode(self, validator_warn):
        """Test validation with warn mode - returns all records."""
        data = [
            {"name": "Alice", "age": 30},
            {"age": 25},  # Missing required 'name'
        ]
        valid, quarantined, error_count = await validator_warn.validate(data)
        # Warn mode returns all records (including invalid)
        assert len(valid) == 2
        assert len(quarantined) == 0
        assert error_count == 1

    @pytest.mark.asyncio
    async def test_validate_quarantine_mode(self, validator_quarantine):
        """Test validation with quarantine mode - separates invalid records."""
        data = [
            {"name": "Alice", "age": 30},
            {"age": 25},  # Missing required 'name'
            {"name": "Charlie", "age": 35},
        ]
        valid, quarantined, error_count = await validator_quarantine.validate(data)
        assert len(valid) == 2  # Alice and Charlie
        assert len(quarantined) == 1  # The invalid record
        assert error_count == 1


class TestResolveSchema:
    """Test schema resolution."""

    def test_resolve_schema_yaml_file(self):
        """Test resolving schema from YAML file."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            yaml.dump(
                {
                    "version": "1.0.0",
                    "type": "object",
                    "properties": {"id": {"type": "integer"}},
                },
                f,
            )
            f.flush()
            try:
                schema = resolve_schema(f.name)
                assert schema["type"] == "object"
                assert "id" in schema["properties"]
            finally:
                os.unlink(f.name)

    def test_resolve_schema_relative_path(self):
        """Test resolving schema with relative path."""
        with tempfile.TemporaryDirectory() as tmpdir:
            schema_path = Path(tmpdir) / "schema.yaml"
            with open(schema_path, "w") as f:
                yaml.dump({"version": "1.0.0", "type": "object"}, f)

            schema = resolve_schema("schema.yaml", base_dir=Path(tmpdir))
            assert schema["type"] == "object"

    def test_resolve_schema_not_found(self):
        """Test resolving non-existent schema file."""
        with pytest.raises(FileNotFoundError):
            resolve_schema("/nonexistent/schema.yaml")


class TestResolveContract:
    """Test contract resolution."""

    def test_resolve_contract_file_path(self):
        """Test resolving contract from file path."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            yaml.dump(
                {
                    "schema": {
                        "version": "1.0.0",
                        "type": "object",
                        "properties": {"id": {"type": "integer"}},
                    }
                },
                f,
            )
            f.flush()
            try:
                contract = resolve_contract(f.name)
                assert "schema" in contract
                assert contract["schema"]["type"] == "object"
            finally:
                os.unlink(f.name)

    def test_resolve_contract_directory(self):
        """Test resolving contract from directory."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Create schema.yaml in directory
            schema_path = Path(tmpdir) / "schema.yaml"
            with open(schema_path, "w") as f:
                yaml.dump(
                    {
                        "version": "1.0.0",
                        "type": "object",
                        "properties": {"name": {"type": "string"}},
                    },
                    f,
                )

            contract = resolve_contract(tmpdir)
            assert "schema" in contract

    def test_resolve_contract_dict_file_type(self):
        """Test resolving contract from dict with file type."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            yaml.dump({"schema": {"version": "1.0.0", "type": "object"}}, f)
            f.flush()
            try:
                contract = resolve_contract({"type": "file", "path": f.name})
                assert "schema" in contract
            finally:
                os.unlink(f.name)

    def test_resolve_contract_missing_path(self):
        """Test resolving contract dict without path."""
        with pytest.raises(ValueError, match="missing 'path' field"):
            resolve_contract({"type": "file"})

    def test_resolve_contract_database_without_store(self):
        """Test resolving database contract without metadata store."""
        with pytest.raises(ValueError, match="metadata_store required"):
            resolve_contract({"type": "database", "name": "orders"})


class TestCreateDLQ:
    """Test DLQ creation."""

    def test_create_dlq_from_bool_true(self):
        """Test creating DLQ from True boolean."""
        dlq = create_dlq(True)
        assert dlq is not None
        assert dlq.storage_backend == "file"

    def test_create_dlq_from_bool_false(self):
        """Test creating DLQ from False boolean."""
        dlq = create_dlq(False)
        assert dlq is None

    def test_create_dlq_from_none(self):
        """Test creating DLQ from None."""
        dlq = create_dlq(None)
        assert dlq is None

    def test_create_dlq_from_config(self):
        """Test creating DLQ from config object."""
        config = DLQConfig(enabled=True, backend="file", path="./dlq")
        dlq = create_dlq(config)
        assert dlq is not None
        assert dlq.storage_backend == "file"

    def test_create_dlq_from_dict(self):
        """Test creating DLQ from dict."""
        dlq = create_dlq({"enabled": True, "backend": "file", "path": "./dlq"})
        assert dlq is not None

    def test_create_dlq_disabled_config(self):
        """Test creating DLQ from disabled config."""
        config = DLQConfig(enabled=False)
        dlq = create_dlq(config)
        assert dlq is None


class TestCreateETLValidator:
    """Test ETL validator factory function."""

    def test_create_etl_validator_extract(self):
        """Test creating extract validator."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            yaml.dump(
                {
                    "version": "1.0.0",
                    "type": "object",
                    "properties": {"name": {"type": "string"}},
                },
                f,
            )
            f.flush()
            try:
                config = ExtractValidationConfig(
                    schema_path=f.name,
                    on_error="skip",
                )
                validator = create_etl_validator(
                    validation_config=config,
                    pipeline_name="test",
                    stage="extract",
                )
                assert validator is not None
                assert validator.on_error == OnErrorAction.SKIP
            finally:
                os.unlink(f.name)

    def test_create_etl_validator_none_config(self):
        """Test creating validator with None config."""
        validator = create_etl_validator(
            validation_config=None,
            pipeline_name="test",
            stage="extract",
        )
        assert validator is None

    def test_create_etl_validator_missing_schema(self):
        """Test creating extract validator without schema."""
        config = ExtractValidationConfig(on_error="skip")
        validator = create_etl_validator(
            validation_config=config,
            pipeline_name="test",
            stage="extract",
        )
        assert validator is None  # Returns None when schema not configured
