"""
Test suite for validator injection decorators.

Tests cover:
- field_validator decorator
- inject_validators function
- apply_validation_to_schema function
- validate_row functions
- Integration with SchemaLoader
"""

import pytest
import tempfile
import yaml
from pathlib import Path

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

from atomingest.schema.validators import (
    FieldValidationConfig,
    LengthValidator,
    RangeValidator,
    RegexValidator,
    ValidationError,
)
from atomingest.schema.decorators import (
    ValidatedFieldDescriptor,
    apply_validation_to_schema,
    field_validator,
    inject_validators,
    validate_row,
    validate_row_strict,
)
from atomingest.core.loader import SchemaLoader


class TestFieldValidatorDecorator:
    """Tests for field_validator decorator."""

    def test_decorator_adds_validation(self):
        """Test decorator adds validation to field class."""

        @field_validator(RegexValidator(r"^[A-Z]+$"))
        class UppercaseOnlyField(fields.CharField):
            pass

        # Create a test model
        class TestModel(Model):
            code = UppercaseOnlyField(max_length=10)

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        # Valid value should work
        instance = TestModel()
        instance.code = "ABC"
        assert instance.code == "ABC"

        # Invalid value should raise
        with pytest.raises(ValidationError):
            instance.code = "abc"

        db.close()

    def test_decorator_with_multiple_validators(self):
        """Test decorator with multiple validators."""

        @field_validator(
            RegexValidator(r"^[A-Z]+$"),
            LengthValidator(min_length=2, max_length=5),
        )
        class CodeField(fields.CharField):
            pass

        class TestModel(Model):
            code = CodeField(max_length=10)

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        instance = TestModel()
        instance.code = "ABC"  # Valid
        assert instance.code == "ABC"

        with pytest.raises(ValidationError):
            instance.code = "A"  # Too short

        with pytest.raises(ValidationError):
            instance.code = "ABCDEFG"  # Too long

        db.close()

    def test_decorator_preserves_original_validation(self):
        """Test decorator preserves original field type validation."""

        @field_validator(RangeValidator(min_value=0, max_value=100))
        class ScoreField(fields.IntegerField):
            pass

        class TestModel(Model):
            score = ScoreField()

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        instance = TestModel()
        instance.score = 50  # Valid
        assert instance.score == 50

        # Custom validation runs first, so it will catch invalid values
        # The RangeValidator will raise ValidationError for non-numeric values
        with pytest.raises((ValueError, ValidationError)):
            instance.score = "not an int"

        db.close()


class TestInjectValidators:
    """Tests for inject_validators function."""

    def test_inject_single_validator(self):
        """Test injecting single validator into field."""
        field_instance = fields.CharField(max_length=10)
        inject_validators(field_instance, [RegexValidator(r"^[A-Z]+$")])

        class TestModel(Model):
            code = field_instance

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        instance = TestModel()
        instance.code = "ABC"  # Valid
        assert instance.code == "ABC"

        with pytest.raises(ValidationError):
            instance.code = "abc"

        db.close()

    def test_inject_multiple_validators(self):
        """Test injecting multiple validators into field."""
        field_instance = fields.IntegerField()
        inject_validators(
            field_instance,
            [
                RangeValidator(min_value=1, max_value=100),
            ],
        )

        class TestModel(Model):
            value = field_instance

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        instance = TestModel()
        instance.value = 50  # Valid

        with pytest.raises(ValidationError):
            instance.value = 0  # Below minimum

        db.close()

    def test_inject_empty_validators(self):
        """Test injecting empty validators list does nothing."""
        field_instance = fields.CharField(max_length=10)
        result = inject_validators(field_instance, [])
        assert result is field_instance

    def test_none_value_passes_validation(self):
        """Test None value passes injected validators."""
        field_instance = fields.CharField(max_length=10, nullable=True)
        inject_validators(field_instance, [RegexValidator(r"^[A-Z]+$")])

        class TestModel(Model):
            code = field_instance

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        instance = TestModel()
        instance.code = None  # Should not raise
        assert instance.code is None

        db.close()


class TestApplyValidationToSchema:
    """Tests for apply_validation_to_schema function."""

    def test_apply_single_field_validation(self):
        """Test applying validation to single field."""
        schema = {
            "code": fields.CharField(max_length=10),
        }
        validation_rules = {
            "code": [{"type": "regex", "pattern": r"^[A-Z]+$"}],
        }

        apply_validation_to_schema(schema, validation_rules)

        class TestModel(Model):
            code = schema["code"]

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        instance = TestModel()
        instance.code = "ABC"  # Valid

        with pytest.raises(ValidationError):
            instance.code = "abc"

        db.close()

    def test_apply_multiple_fields_validation(self):
        """Test applying validation to multiple fields."""
        schema = {
            "code": fields.CharField(max_length=10),
            "score": fields.IntegerField(),
        }
        validation_rules = {
            "code": [{"type": "regex", "pattern": r"^[A-Z]+$"}],
            "score": [{"type": "range", "min": 0, "max": 100}],
        }

        apply_validation_to_schema(schema, validation_rules)

        class TestModel(Model):
            code = schema["code"]
            score = schema["score"]

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        instance = TestModel()
        instance.code = "ABC"
        instance.score = 50

        with pytest.raises(ValidationError):
            instance.code = "abc"

        with pytest.raises(ValidationError):
            instance.score = 150

        db.close()

    def test_apply_validation_unknown_field_warning(self, caplog):
        """Test warning logged for unknown field in validation rules."""
        import logging

        schema = {
            "code": fields.CharField(max_length=10),
        }
        validation_rules = {
            "unknown_field": [{"type": "regex", "pattern": r"^.*$"}],
        }

        with caplog.at_level(logging.WARNING):
            apply_validation_to_schema(schema, validation_rules)

        assert any(
            "unknown field" in record.message.lower() for record in caplog.records
        )


class TestValidateRow:
    """Tests for validate_row function."""

    def test_validate_row_all_valid(self):
        """Test validate_row with all valid values."""
        configs = {
            "code": FieldValidationConfig(
                field_name="code",
                validators=[RegexValidator(r"^[A-Z]+$")],
            ),
            "score": FieldValidationConfig(
                field_name="score",
                validators=[RangeValidator(min_value=0, max_value=100)],
            ),
        }
        row = {"code": "ABC", "score": 50}

        errors = validate_row(row, configs)
        assert len(errors) == 0

    def test_validate_row_with_errors(self):
        """Test validate_row collects all errors."""
        configs = {
            "code": FieldValidationConfig(
                field_name="code",
                validators=[RegexValidator(r"^[A-Z]+$")],
            ),
            "score": FieldValidationConfig(
                field_name="score",
                validators=[RangeValidator(min_value=0, max_value=100)],
            ),
        }
        row = {"code": "abc", "score": 150}  # Both invalid

        errors = validate_row(row, configs)
        assert len(errors) == 2

    def test_validate_row_missing_field(self):
        """Test validate_row with missing field value."""
        configs = {
            "code": FieldValidationConfig(
                field_name="code",
                validators=[RegexValidator(r"^[A-Z]+$")],
            ),
        }
        row = {}  # Missing 'code'

        errors = validate_row(row, configs)
        # None value should pass validator (nullable handled elsewhere)
        assert len(errors) == 0


class TestValidateRowStrict:
    """Tests for validate_row_strict function."""

    def test_validate_row_strict_valid(self):
        """Test validate_row_strict with valid values."""
        configs = {
            "code": FieldValidationConfig(
                field_name="code",
                validators=[RegexValidator(r"^[A-Z]+$")],
            ),
        }
        row = {"code": "ABC"}

        validate_row_strict(row, configs)  # Should not raise

    def test_validate_row_strict_raises_on_first_error(self):
        """Test validate_row_strict raises on first error."""
        configs = {
            "code": FieldValidationConfig(
                field_name="code",
                validators=[RegexValidator(r"^[A-Z]+$")],
            ),
            "score": FieldValidationConfig(
                field_name="score",
                validators=[RangeValidator(min_value=0, max_value=100)],
            ),
        }
        row = {"code": "abc", "score": 150}  # Both invalid

        with pytest.raises(ValidationError):
            validate_row_strict(row, configs)


class TestValidatedFieldDescriptor:
    """Tests for ValidatedFieldDescriptor."""

    def test_descriptor_wraps_field(self):
        """Test descriptor wraps field correctly."""
        base_field = fields.CharField(max_length=10)
        validators = [RegexValidator(r"^[A-Z]+$")]

        descriptor = ValidatedFieldDescriptor(base_field, validators)

        class TestModel(Model):
            code = descriptor

        db = Database("sqlite:///:memory:")
        db.register(TestModel)

        instance = TestModel()
        instance.code = "ABC"  # Valid
        assert instance.code == "ABC"

        with pytest.raises(ValidationError):
            instance.code = "abc"

        db.close()


class TestSchemaLoaderIntegration:
    """Integration tests for SchemaLoader with validators."""

    @pytest.fixture
    def temp_dir(self):
        """Create temporary directory for test files."""
        with tempfile.TemporaryDirectory() as tmpdir:
            yield Path(tmpdir)

    def test_load_schema_with_validation(self, temp_dir):
        """Test loading schema with validation rules from YAML."""
        config = {
            "target_table": "accounts",
            "schema": {
                "account_code": {
                    "type": "CharField",
                    "max_length": 10,
                    "validation": [
                        {
                            "type": "regex",
                            "pattern": "^[A-Z]{2}[0-9]{6}$",
                            "message": "Invalid account code format",
                        }
                    ],
                },
                "balance": {
                    "type": "IntegerField",
                    "validation": [{"type": "range", "min": 0}],
                },
            },
        }
        config_file = temp_dir / "config.yaml"
        with open(config_file, "w") as f:
            yaml.dump(config, f)

        loader = SchemaLoader()
        schema_def = loader.load_from_yaml(str(config_file))

        assert "validation" in schema_def
        assert "account_code" in schema_def["validation"]
        assert "balance" in schema_def["validation"]

    def test_create_model_with_validated_fields(self, temp_dir):
        """Test creating model with validated fields."""
        config = {
            "target_table": "test_table",
            "schema": {
                "code": {
                    "type": "CharField",
                    "max_length": 10,
                    "validation": [{"type": "regex", "pattern": "^[A-Z]+$"}],
                },
            },
        }
        config_file = temp_dir / "config.yaml"
        with open(config_file, "w") as f:
            yaml.dump(config, f)

        loader = SchemaLoader()
        schema_def = loader.load_from_yaml(str(config_file))
        ModelClass = loader.create_model(schema_def)

        db = Database("sqlite:///:memory:")
        db.register(ModelClass)

        instance = ModelClass()
        instance.code = "ABC"  # Valid
        assert instance.code == "ABC"

        with pytest.raises(ValidationError):
            instance.code = "abc"

        db.close()

    def test_validation_with_choice_validator(self, temp_dir):
        """Test choice validator from YAML config."""
        config = {
            "target_table": "orders",
            "schema": {
                "status": {
                    "type": "CharField",
                    "max_length": 20,
                    "validation": [
                        {
                            "type": "choice",
                            "choices": ["pending", "approved", "rejected"],
                            "case_sensitive": False,
                        }
                    ],
                },
            },
        }
        config_file = temp_dir / "config.yaml"
        with open(config_file, "w") as f:
            yaml.dump(config, f)

        loader = SchemaLoader()
        schema_def = loader.load_from_yaml(str(config_file))
        ModelClass = loader.create_model(schema_def)

        db = Database("sqlite:///:memory:")
        db.register(ModelClass)

        instance = ModelClass()
        instance.status = "PENDING"  # Case-insensitive
        assert instance.status == "PENDING"

        with pytest.raises(ValidationError):
            instance.status = "invalid"

        db.close()

    def test_validation_with_multiple_rules(self, temp_dir):
        """Test multiple validation rules on a single field."""
        config = {
            "target_table": "products",
            "schema": {
                "sku": {
                    "type": "CharField",
                    "max_length": 20,
                    "validation": [
                        {"type": "not_empty"},
                        {"type": "regex", "pattern": "^SKU-[0-9]{4}$"},
                        {"type": "length", "min_length": 8, "max_length": 8},
                    ],
                },
            },
        }
        config_file = temp_dir / "config.yaml"
        with open(config_file, "w") as f:
            yaml.dump(config, f)

        loader = SchemaLoader()
        schema_def = loader.load_from_yaml(str(config_file))
        ModelClass = loader.create_model(schema_def)

        db = Database("sqlite:///:memory:")
        db.register(ModelClass)

        instance = ModelClass()
        instance.sku = "SKU-1234"  # Valid
        assert instance.sku == "SKU-1234"

        with pytest.raises(ValidationError):
            instance.sku = ""  # Empty

        with pytest.raises(ValidationError):
            instance.sku = "INVALID"  # Wrong format

        db.close()

    def test_invalid_validation_config_raises(self, temp_dir):
        """Test invalid validation config raises ImproperlyConfigured."""
        from atomsql.utils.exceptions import ImproperlyConfigured

        config = {
            "target_table": "test_table",
            "schema": {
                "code": {
                    "type": "CharField",
                    "max_length": 10,
                    "validation": [{"type": "unknown_validator"}],
                },
            },
        }
        config_file = temp_dir / "config.yaml"
        with open(config_file, "w") as f:
            yaml.dump(config, f)

        loader = SchemaLoader()
        with pytest.raises(ImproperlyConfigured):
            loader.load_from_yaml(str(config_file))

    def test_schema_without_validation(self, temp_dir):
        """Test schema without validation rules still works."""
        config = {
            "target_table": "simple_table",
            "schema": {
                "name": {"type": "CharField", "max_length": 50},
                "age": {"type": "IntegerField"},
            },
        }
        config_file = temp_dir / "config.yaml"
        with open(config_file, "w") as f:
            yaml.dump(config, f)

        loader = SchemaLoader()
        schema_def = loader.load_from_yaml(str(config_file))

        assert schema_def["validation"] == {}
        assert "name" in schema_def["schema"]
        assert "age" in schema_def["schema"]
