"""
Test suite for validator classes.

Tests cover:
- Individual validator behavior (regex, range, length, choice, not_empty)
- Validation error handling
- Validator creation from YAML-style dictionaries
- Field validation configuration
"""

import pytest
from decimal import Decimal

from atomingest.schema.validators import (
    ChoiceValidator,
    CustomValidator,
    FieldValidationConfig,
    LengthValidator,
    NotEmptyValidator,
    RangeValidator,
    RegexValidator,
    ValidationError,
    Validator,
    VALIDATOR_REGISTRY,
    create_validator_from_dict,
    parse_field_validators,
)


class TestValidationError:
    """Tests for ValidationError exception."""

    def test_basic_error(self):
        """Test basic error message."""
        error = ValidationError("Invalid value")
        assert "Invalid value" in str(error)

    def test_error_with_field_name(self):
        """Test error includes field name."""
        error = ValidationError("Invalid value", field_name="email")
        assert "email" in str(error)
        assert "Invalid value" in str(error)

    def test_error_attributes(self):
        """Test error stores attributes correctly."""
        error = ValidationError("Test", field_name="test_field", value=123)
        assert error.field_name == "test_field"
        assert error.value == 123
        assert error.message == "Test"


class TestRegexValidator:
    """Tests for RegexValidator."""

    def test_valid_pattern_match(self):
        """Test value matching pattern passes."""
        validator = RegexValidator(r"^[A-Z]{3}$")
        validator("ABC", "code")  # Should not raise

    def test_invalid_pattern_match(self):
        """Test value not matching pattern raises error."""
        validator = RegexValidator(r"^[A-Z]{3}$")
        with pytest.raises(ValidationError) as exc_info:
            validator("abc", "code")
        assert "code" in str(exc_info.value)

    def test_none_value_allowed(self):
        """Test None value passes (nullable handled elsewhere)."""
        validator = RegexValidator(r"^[A-Z]+$")
        validator(None, "field")  # Should not raise

    def test_custom_error_message(self):
        """Test custom error message is used."""
        validator = RegexValidator(r"^[A-Z]+$", message="Must be uppercase letters")
        with pytest.raises(ValidationError) as exc_info:
            validator("123", "field")
        assert "Must be uppercase letters" in str(exc_info.value)

    def test_case_insensitive_flag(self):
        """Test regex with case-insensitive flag."""
        import re

        validator = RegexValidator(r"^hello$", flags=re.IGNORECASE)
        validator("HELLO", "field")  # Should not raise
        validator("hello", "field")  # Should not raise

    def test_compiled_pattern(self):
        """Test using pre-compiled pattern."""
        import re

        pattern = re.compile(r"^\d{4}-\d{2}-\d{2}$")
        validator = RegexValidator(pattern)
        validator("2026-01-18", "date")  # Should not raise

    def test_account_code_pattern(self):
        """Test real-world account code pattern."""
        validator = RegexValidator(
            r"^[A-Z]{2}[0-9]{6}$", message="Must be 2 letters + 6 digits"
        )
        validator("AB123456", "account_code")  # Valid
        with pytest.raises(ValidationError):
            validator("ab123456", "account_code")  # Lowercase
        with pytest.raises(ValidationError):
            validator("ABC12345", "account_code")  # Wrong format


class TestRangeValidator:
    """Tests for RangeValidator."""

    def test_value_in_range(self):
        """Test value within range passes."""
        validator = RangeValidator(min_value=0, max_value=100)
        validator(50, "score")  # Should not raise
        validator(0, "score")  # Min boundary
        validator(100, "score")  # Max boundary

    def test_value_below_minimum(self):
        """Test value below minimum raises error."""
        validator = RangeValidator(min_value=0)
        with pytest.raises(ValidationError) as exc_info:
            validator(-1, "score")
        assert "less than minimum" in str(exc_info.value)

    def test_value_above_maximum(self):
        """Test value above maximum raises error."""
        validator = RangeValidator(max_value=100)
        with pytest.raises(ValidationError) as exc_info:
            validator(101, "score")
        assert "greater than maximum" in str(exc_info.value)

    def test_min_only(self):
        """Test with only minimum specified."""
        validator = RangeValidator(min_value=0)
        validator(1000000, "value")  # Large value should pass

    def test_max_only(self):
        """Test with only maximum specified."""
        validator = RangeValidator(max_value=100)
        validator(-1000, "value")  # Negative value should pass

    def test_float_values(self):
        """Test with float values."""
        validator = RangeValidator(min_value=0.0, max_value=1.0)
        validator(0.5, "probability")  # Should not raise
        with pytest.raises(ValidationError):
            validator(1.1, "probability")

    def test_decimal_values(self):
        """Test with Decimal values."""
        validator = RangeValidator(min_value=Decimal("0"), max_value=Decimal("999.99"))
        validator(Decimal("500.50"), "price")  # Should not raise

    def test_none_value_allowed(self):
        """Test None value passes."""
        validator = RangeValidator(min_value=0, max_value=100)
        validator(None, "field")  # Should not raise

    def test_no_bounds_raises(self):
        """Test that no bounds raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            RangeValidator()
        assert "min_value or max_value" in str(exc_info.value)

    def test_non_numeric_value(self):
        """Test non-numeric value raises error."""
        validator = RangeValidator(min_value=0, max_value=100)
        with pytest.raises(ValidationError) as exc_info:
            validator("not a number", "field")
        assert "not a valid number" in str(exc_info.value)


class TestLengthValidator:
    """Tests for LengthValidator."""

    def test_length_in_range(self):
        """Test string with valid length passes."""
        validator = LengthValidator(min_length=3, max_length=10)
        validator("hello", "name")  # Should not raise

    def test_length_below_minimum(self):
        """Test string below minimum length raises error."""
        validator = LengthValidator(min_length=5)
        with pytest.raises(ValidationError) as exc_info:
            validator("hi", "name")
        assert "less than minimum" in str(exc_info.value)

    def test_length_above_maximum(self):
        """Test string above maximum length raises error."""
        validator = LengthValidator(max_length=5)
        with pytest.raises(ValidationError) as exc_info:
            validator("hello world", "name")
        assert "greater than maximum" in str(exc_info.value)

    def test_exact_boundary_lengths(self):
        """Test exact boundary lengths pass."""
        validator = LengthValidator(min_length=3, max_length=5)
        validator("abc", "field")  # Exactly min
        validator("abcde", "field")  # Exactly max

    def test_none_value_allowed(self):
        """Test None value passes."""
        validator = LengthValidator(min_length=1, max_length=10)
        validator(None, "field")  # Should not raise

    def test_no_bounds_raises(self):
        """Test that no bounds raises ValueError."""
        with pytest.raises(ValueError):
            LengthValidator()


class TestChoiceValidator:
    """Tests for ChoiceValidator."""

    def test_valid_choice(self):
        """Test valid choice passes."""
        validator = ChoiceValidator(choices=["active", "inactive", "pending"])
        validator("active", "status")  # Should not raise

    def test_invalid_choice(self):
        """Test invalid choice raises error."""
        validator = ChoiceValidator(choices=["active", "inactive"])
        with pytest.raises(ValidationError) as exc_info:
            validator("unknown", "status")
        assert "not one of the allowed choices" in str(exc_info.value)

    def test_case_sensitive(self):
        """Test case-sensitive matching (default)."""
        validator = ChoiceValidator(choices=["Active", "Inactive"])
        validator("Active", "status")  # Should not raise
        with pytest.raises(ValidationError):
            validator("active", "status")  # Different case

    def test_case_insensitive(self):
        """Test case-insensitive matching."""
        validator = ChoiceValidator(
            choices=["Active", "Inactive"], case_sensitive=False
        )
        validator("ACTIVE", "status")  # Should not raise
        validator("active", "status")  # Should not raise

    def test_numeric_choices(self):
        """Test with numeric choices."""
        validator = ChoiceValidator(choices=[1, 2, 3])
        validator(2, "level")  # Should not raise
        with pytest.raises(ValidationError):
            validator(4, "level")

    def test_none_value_allowed(self):
        """Test None value passes."""
        validator = ChoiceValidator(choices=["a", "b"])
        validator(None, "field")  # Should not raise

    def test_empty_choices_raises(self):
        """Test empty choices list raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            ChoiceValidator(choices=[])
        assert "cannot be empty" in str(exc_info.value)


class TestNotEmptyValidator:
    """Tests for NotEmptyValidator."""

    def test_non_empty_string(self):
        """Test non-empty string passes."""
        validator = NotEmptyValidator()
        validator("hello", "name")  # Should not raise

    def test_empty_string_fails(self):
        """Test empty string raises error."""
        validator = NotEmptyValidator()
        with pytest.raises(ValidationError) as exc_info:
            validator("", "name")
        assert "cannot be empty" in str(exc_info.value)

    def test_whitespace_only_fails(self):
        """Test whitespace-only string raises error."""
        validator = NotEmptyValidator()
        with pytest.raises(ValidationError):
            validator("   ", "name")

    def test_none_value_allowed(self):
        """Test None value passes (handled by nullable)."""
        validator = NotEmptyValidator()
        validator(None, "field")  # Should not raise

    def test_empty_list_fails(self):
        """Test empty list raises error."""
        validator = NotEmptyValidator()
        with pytest.raises(ValidationError):
            validator([], "items")

    def test_non_empty_list(self):
        """Test non-empty list passes."""
        validator = NotEmptyValidator()
        validator([1, 2, 3], "items")  # Should not raise


class TestCustomValidator:
    """Tests for CustomValidator."""

    def test_valid_custom_function(self):
        """Test custom function returning True passes."""
        validator = CustomValidator(func=lambda x: x > 0)
        validator(10, "value")  # Should not raise

    def test_invalid_custom_function(self):
        """Test custom function returning False raises error."""
        validator = CustomValidator(func=lambda x: x > 0)
        with pytest.raises(ValidationError):
            validator(-5, "value")

    def test_custom_function_raising_exception(self):
        """Test custom function raising exception is caught."""

        def check(x):
            raise ValueError("Custom check failed")

        validator = CustomValidator(func=check)
        with pytest.raises(ValidationError) as exc_info:
            validator(10, "value")
        assert "Custom validation failed" in str(exc_info.value)

    def test_none_value_allowed(self):
        """Test None value passes."""
        validator = CustomValidator(func=lambda x: x > 0)
        validator(None, "field")  # Should not raise

    def test_non_callable_raises(self):
        """Test non-callable raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            CustomValidator(func="not a function")
        assert "must be a callable" in str(exc_info.value)


class TestCreateValidatorFromDict:
    """Tests for create_validator_from_dict function."""

    def test_create_regex_validator(self):
        """Test creating RegexValidator from dict."""
        config = {"type": "regex", "pattern": r"^\d+$"}
        validator = create_validator_from_dict(config)
        assert isinstance(validator, RegexValidator)
        validator("123", "field")  # Should not raise

    def test_create_range_validator(self):
        """Test creating RangeValidator from dict."""
        config = {"type": "range", "min": 0, "max": 100}
        validator = create_validator_from_dict(config)
        assert isinstance(validator, RangeValidator)
        validator(50, "field")  # Should not raise

    def test_create_length_validator(self):
        """Test creating LengthValidator from dict."""
        config = {"type": "length", "min_length": 3, "max_length": 10}
        validator = create_validator_from_dict(config)
        assert isinstance(validator, LengthValidator)

    def test_create_choice_validator(self):
        """Test creating ChoiceValidator from dict."""
        config = {
            "type": "choice",
            "choices": ["a", "b", "c"],
            "case_sensitive": False,
        }
        validator = create_validator_from_dict(config)
        assert isinstance(validator, ChoiceValidator)

    def test_create_not_empty_validator(self):
        """Test creating NotEmptyValidator from dict."""
        config = {"type": "not_empty"}
        validator = create_validator_from_dict(config)
        assert isinstance(validator, NotEmptyValidator)

    def test_custom_error_message(self):
        """Test custom error message in config."""
        config = {
            "type": "regex",
            "pattern": r"^\d+$",
            "message": "Must be digits only",
        }
        validator = create_validator_from_dict(config)
        with pytest.raises(ValidationError) as exc_info:
            validator("abc", "field")
        assert "Must be digits only" in str(exc_info.value)

    def test_unknown_type_raises(self):
        """Test unknown validator type raises error."""
        config = {"type": "unknown_validator"}
        with pytest.raises(ValueError) as exc_info:
            create_validator_from_dict(config)
        assert "Unknown validator type" in str(exc_info.value)

    def test_missing_type_raises(self):
        """Test missing type raises error."""
        config = {"pattern": r"^\d+$"}
        with pytest.raises(ValueError) as exc_info:
            create_validator_from_dict(config)
        assert "must include 'type'" in str(exc_info.value)

    def test_invalid_config_raises(self):
        """Test invalid config for validator type raises error."""
        config = {"type": "range"}  # Missing min or max
        with pytest.raises(ValueError) as exc_info:
            create_validator_from_dict(config)
        # Error can come from validator constructor or wrapper
        assert "min" in str(exc_info.value).lower() or "Invalid" in str(exc_info.value)


class TestFieldValidationConfig:
    """Tests for FieldValidationConfig."""

    def test_validate_with_all_passing(self):
        """Test validation passes when all validators pass."""
        config = FieldValidationConfig(
            field_name="code",
            validators=[
                RegexValidator(r"^[A-Z]+$"),
                LengthValidator(min_length=2, max_length=5),
            ],
        )
        config.validate("ABC")  # Should not raise

    def test_validate_with_first_failing(self):
        """Test validation fails on first failing validator."""
        config = FieldValidationConfig(
            field_name="code",
            validators=[
                RegexValidator(r"^[A-Z]+$"),
                LengthValidator(min_length=2),
            ],
        )
        with pytest.raises(ValidationError):
            config.validate("abc")  # Fails regex

    def test_validate_none_value(self):
        """Test None value passes all validators."""
        config = FieldValidationConfig(
            field_name="code",
            validators=[
                RegexValidator(r"^[A-Z]+$"),
                NotEmptyValidator(),
            ],
        )
        config.validate(None)  # Should not raise


class TestParseFieldValidators:
    """Tests for parse_field_validators function."""

    def test_parse_single_validator(self):
        """Test parsing single validator config."""
        config = [{"type": "regex", "pattern": r"^\d+$"}]
        field_config = parse_field_validators("test_field", config)
        assert field_config.field_name == "test_field"
        assert len(field_config.validators) == 1

    def test_parse_multiple_validators(self):
        """Test parsing multiple validator configs."""
        config = [
            {"type": "regex", "pattern": r"^\d+$"},
            {"type": "range", "min": 0, "max": 100},
        ]
        field_config = parse_field_validators("test_field", config)
        assert len(field_config.validators) == 2

    def test_parse_invalid_config_raises(self):
        """Test invalid config raises error."""
        config = [{"type": "unknown"}]
        with pytest.raises(ValueError):
            parse_field_validators("test_field", config)


class TestValidatorRegistry:
    """Tests for VALIDATOR_REGISTRY."""

    def test_all_validators_registered(self):
        """Test all expected validators are in registry."""
        expected = {"regex", "range", "length", "choice", "not_empty"}
        assert set(VALIDATOR_REGISTRY.keys()) == expected

    def test_registry_values_are_classes(self):
        """Test registry values are validator classes."""
        for name, cls in VALIDATOR_REGISTRY.items():
            assert issubclass(cls, Validator)
