"""Unit tests for validation/schema.py module.

Tests MetricNamingConvention class and validate_metric_name function
covering metric prefix routing, naming pattern validation, security checks,
and protobuf schema validation.
"""

import pytest

from mca_sdk.validation.schema import (
    MetricNamingConvention,
    validate_metric_name,
    METRIC_PATTERNS,
    VALID_UNITS,
    VALID_PREFIXES,
)
from mca_sdk.utils.exceptions import ValidationError


class TestMetricNamingConventionInit:
    """Test MetricNamingConvention initialization and prefix resolution."""

    def test_init_with_default_taxonomy(self):
        """Test initialization with default internal/regression taxonomy."""
        validator = MetricNamingConvention()

        assert validator.model_category == "internal"
        assert validator.model_type == "regression"
        assert validator.expected_prefix == "model"
        assert validator.strict is True
        assert validator.patterns == METRIC_PATTERNS["model"]

    def test_init_with_internal_generative(self):
        """Test initialization with internal/generative taxonomy."""
        validator = MetricNamingConvention(model_category="internal", model_type="generative")

        assert validator.expected_prefix == "genai"
        assert validator.patterns == METRIC_PATTERNS["genai"]

    def test_init_with_internal_agentic(self):
        """Test initialization with internal/agentic taxonomy."""
        validator = MetricNamingConvention(model_category="internal", model_type="agentic")

        assert validator.expected_prefix == "agentic"
        assert validator.patterns == METRIC_PATTERNS["agentic"]

    def test_init_with_vendor_taxonomy(self):
        """Test initialization with vendor taxonomy."""
        validator = MetricNamingConvention(model_category="vendor", model_type="classification")

        assert validator.expected_prefix == "vendor"
        assert validator.patterns == METRIC_PATTERNS["vendor"]

    def test_init_non_strict_mode(self):
        """Test initialization with strict=False."""
        validator = MetricNamingConvention(strict=False)

        assert validator.strict is False

    def test_init_with_invalid_taxonomy_raises_error(self):
        """Test that invalid taxonomy combination raises ValidationError."""
        with pytest.raises(ValidationError, match="Invalid taxonomy combination"):
            MetricNamingConvention(model_category="invalid", model_type="unknown")


class TestMetricNamingConventionValidate:
    """Test metric name validation against patterns."""

    def test_validate_valid_model_metrics(self):
        """Test validation of valid model.* metric names."""
        validator = MetricNamingConvention(model_category="internal", model_type="regression")

        # Should not raise
        validator.validate("model.predictions_total")
        validator.validate("model.latency_seconds")
        validator.validate("model.accuracy_ratio")
        validator.validate("model.error_count")
        validator.validate("model.inference_milliseconds")

    def test_validate_valid_genai_metrics(self):
        """Test validation of valid genai.* metric names."""
        validator = MetricNamingConvention(model_category="internal", model_type="generative")

        # Should not raise
        validator.validate("genai.requests_total")
        validator.validate("genai.latency_seconds")
        validator.validate("genai.success_ratio")
        validator.validate("genai.token_count")
        validator.validate("genai.cost_dollars")

    def test_validate_valid_agentic_metrics(self):
        """Test validation of valid agentic.* metric names."""
        validator = MetricNamingConvention(model_category="internal", model_type="agentic")

        # Should not raise
        validator.validate("agentic.goals_total")
        validator.validate("agentic.reasoning_seconds")
        validator.validate("agentic.success_ratio")
        validator.validate("agentic.tool_count")
        validator.validate("agentic.cost_dollars")

    def test_validate_valid_vendor_metrics(self):
        """Test validation of valid vendor.* metric names."""
        validator = MetricNamingConvention(model_category="vendor", model_type="classification")

        # Should not raise
        validator.validate("vendor.predictions_total")
        validator.validate("vendor.latency_seconds")
        validator.validate("vendor.accuracy_ratio")
        validator.validate("vendor.error_count")

    def test_validate_invalid_metric_name_raises_error(self):
        """Test that invalid metric name raises ValidationError."""
        validator = MetricNamingConvention(model_category="internal", model_type="regression")

        with pytest.raises(ValidationError, match="Invalid metric name"):
            validator.validate("custom_metric")

    def test_validate_wrong_prefix_raises_error(self):
        """Test that wrong prefix raises ValidationError."""
        validator = MetricNamingConvention(model_category="internal", model_type="regression")

        with pytest.raises(ValidationError, match="Invalid metric name"):
            validator.validate("genai.requests_total")

    def test_validate_wrong_suffix_raises_error(self):
        """Test that wrong suffix raises ValidationError."""
        validator = MetricNamingConvention(model_category="internal", model_type="regression")

        with pytest.raises(ValidationError, match="Invalid metric name"):
            validator.validate("model.predictions_invalid")

    def test_validate_non_strict_mode_allows_any_safe_name(self):
        """Test that non-strict mode allows any safe metric name."""
        validator = MetricNamingConvention(strict=False)

        # Should not raise - any safe name allowed
        validator.validate("custom_metric")
        validator.validate("my.custom.metric_total")
        validator.validate("anything_with-hyphen")  # Hyphens allowed in non-strict

    def test_validate_nested_namespaces(self):
        """Test validation of metrics with nested namespaces."""
        validator = MetricNamingConvention(model_category="internal", model_type="regression")

        # Nested namespaces should work
        validator.validate("model.layer1.predictions_total")
        validator.validate("model.pipeline.stage1.latency_seconds")


class TestSecurityValidation:
    """Test security validation (DoS prevention, input sanitization)."""

    def test_validate_non_string_name_raises_error(self):
        """Test that non-string metric name raises ValidationError."""
        validator = MetricNamingConvention()

        with pytest.raises(ValidationError, match="must be a string"):
            validator.validate(123)

        with pytest.raises(ValidationError, match="must be a string"):
            validator.validate(["metric"])

    def test_validate_length_exceeds_maximum(self):
        """Test that metric name exceeding max length raises ValidationError."""
        validator = MetricNamingConvention()

        long_name = "model." + ("x" * 250) + "_total"
        assert len(long_name) > 256

        with pytest.raises(ValidationError, match="exceeds maximum length"):
            validator.validate(long_name)

    def test_validate_dangerous_characters_rejected(self):
        """Test that dangerous characters are rejected."""
        validator = MetricNamingConvention()

        dangerous_names = [
            "model.test\x00_total",  # null byte
            "model.test\n_total",  # newline
            'model.test"_total',  # quote
            "model.test'_total",  # quote
            "model.test;_total",  # semicolon
            "model.test|_total",  # pipe
            "model.test&_total",  # ampersand
            "model.test$_total",  # dollar
            "model.test`_total",  # backtick
        ]

        for dangerous_name in dangerous_names:
            with pytest.raises(ValidationError, match="dangerous character"):
                validator.validate(dangerous_name)

    def test_validate_invalid_characters_strict_mode(self):
        """Test that invalid characters are rejected in strict mode."""
        validator = MetricNamingConvention(strict=True)

        # Hyphens not allowed in strict mode
        with pytest.raises(ValidationError, match="invalid character"):
            validator.validate("model.test-metric_total")

    def test_validate_hyphens_allowed_non_strict(self):
        """Test that hyphens are allowed in non-strict mode."""
        validator = MetricNamingConvention(strict=False)

        # Should not raise
        validator.validate("model.test-metric_total")
        validator.validate("custom-metric-name")

    def test_validate_control_characters_rejected(self):
        """Test that control characters are rejected."""
        validator = MetricNamingConvention()

        # Control characters (ASCII < 32)
        for i in range(32):
            with pytest.raises(ValidationError, match="dangerous character"):
                validator.validate(f"model.test{chr(i)}_total")


class TestProtobufSchemaValidation:
    """Test protobuf schema structure validation."""

    def test_validate_with_valid_metric_data(self):
        """Test validation with valid metric data structure."""
        validator = MetricNamingConvention()

        metric_data = {
            "name": "model.predictions_total",
            "unit": "total",
            "value": 42,
            "attributes": {"model_id": "mdl-001"},
        }

        # Should not raise
        validator.validate("model.predictions_total", metric_data=metric_data)

    def test_validate_metric_data_not_dict_raises_error(self):
        """Test that non-dict metric_data raises ValidationError."""
        validator = MetricNamingConvention()

        with pytest.raises(ValidationError, match="must be a dict"):
            validator.validate("model.predictions_total", metric_data="not_a_dict")

        with pytest.raises(ValidationError, match="must be a dict"):
            validator.validate("model.predictions_total", metric_data=[1, 2, 3])

    def test_validate_metric_data_name_not_string(self):
        """Test that non-string 'name' field raises ValidationError."""
        validator = MetricNamingConvention()

        metric_data = {"name": 123}

        with pytest.raises(ValidationError, match="'name' field must be a string"):
            validator.validate("model.predictions_total", metric_data=metric_data)

    def test_validate_metric_data_unit_not_string(self):
        """Test that non-string 'unit' field raises ValidationError."""
        validator = MetricNamingConvention()

        metric_data = {"unit": 123}

        with pytest.raises(ValidationError, match="'unit' field must be a string"):
            validator.validate("model.predictions_total", metric_data=metric_data)

    def test_validate_metric_data_invalid_unit_strict_mode(self):
        """Test that invalid unit raises ValidationError in strict mode."""
        validator = MetricNamingConvention(strict=True)

        metric_data = {"unit": "invalid_unit"}

        with pytest.raises(ValidationError, match="Invalid metric unit"):
            validator.validate("model.predictions_total", metric_data=metric_data)

    def test_validate_metric_data_valid_units_accepted(self):
        """Test that valid units are accepted."""
        validator = MetricNamingConvention()

        for unit in VALID_UNITS:
            metric_data = {"unit": unit}
            # Should not raise
            validator.validate("model.predictions_total", metric_data=metric_data)

    def test_validate_metric_data_empty_unit_allowed(self):
        """Test that empty unit string is allowed."""
        validator = MetricNamingConvention()

        metric_data = {"unit": ""}

        # Should not raise
        validator.validate("model.predictions_total", metric_data=metric_data)

    def test_validate_metric_data_value_not_numeric(self):
        """Test that non-numeric 'value' field raises ValidationError."""
        validator = MetricNamingConvention()

        metric_data = {"value": "not_a_number"}

        with pytest.raises(ValidationError, match="'value' field must be numeric"):
            validator.validate("model.predictions_total", metric_data=metric_data)

    def test_validate_metric_data_value_int_and_float_accepted(self):
        """Test that both int and float values are accepted."""
        validator = MetricNamingConvention()

        # Int value
        metric_data = {"value": 42}
        validator.validate("model.predictions_total", metric_data=metric_data)

        # Float value
        metric_data = {"value": 3.14}
        validator.validate("model.predictions_total", metric_data=metric_data)

    def test_validate_metric_data_attributes_not_dict(self):
        """Test that non-dict 'attributes' field raises ValidationError."""
        validator = MetricNamingConvention()

        metric_data = {"attributes": "not_a_dict"}

        with pytest.raises(ValidationError, match="'attributes' field must be a dict"):
            validator.validate("model.predictions_total", metric_data=metric_data)


class TestErrorMessages:
    """Test error message content and helpfulness."""

    def test_error_message_includes_examples(self):
        """Test that error message includes helpful examples."""
        validator = MetricNamingConvention(model_category="internal", model_type="regression")

        try:
            validator.validate("invalid_metric")
        except ValidationError as e:
            error_msg = str(e)
            assert "model.predictions_total" in error_msg
            assert "model.latency_seconds" in error_msg
            assert "model.accuracy_ratio" in error_msg
            assert "strict_validation=False" in error_msg

    def test_error_message_includes_taxonomy(self):
        """Test that error message includes taxonomy info."""
        validator = MetricNamingConvention(model_category="internal", model_type="generative")

        try:
            validator.validate("invalid_metric")
        except ValidationError as e:
            error_msg = str(e)
            assert "model_category='internal'" in error_msg
            assert "model_type='generative'" in error_msg
            assert "genai.*" in error_msg

    def test_error_message_includes_cost_for_genai(self):
        """Test that error message includes cost_dollars for GenAI."""
        validator = MetricNamingConvention(model_category="internal", model_type="generative")

        try:
            validator.validate("invalid_metric")
        except ValidationError as e:
            error_msg = str(e)
            assert "genai.*_dollars" in error_msg
            assert "genai.cost_dollars" in error_msg


class TestGetExamplesMethod:
    """Test _get_examples() method returns correct examples."""

    def test_get_examples_for_model_prefix(self):
        """Test examples for model prefix."""
        validator = MetricNamingConvention(model_category="internal", model_type="regression")

        examples = validator._get_examples()

        assert "model.predictions_total" in examples
        assert "model.latency_seconds" in examples
        assert "model.accuracy_ratio" in examples
        assert "model.error_count" in examples

    def test_get_examples_for_genai_prefix(self):
        """Test examples for genai prefix."""
        validator = MetricNamingConvention(model_category="internal", model_type="generative")

        examples = validator._get_examples()

        assert "genai.requests_total" in examples
        assert "genai.latency_seconds" in examples
        assert "genai.token_count" in examples
        assert "genai.cost_dollars" in examples

    def test_get_examples_for_vendor_prefix(self):
        """Test examples for vendor prefix."""
        validator = MetricNamingConvention(model_category="vendor", model_type="classification")

        examples = validator._get_examples()

        assert "vendor.predictions_total" in examples
        assert "vendor.latency_seconds" in examples
        assert "vendor.accuracy_ratio" in examples


class TestValidateMetricNameFunction:
    """Test convenience function validate_metric_name()."""

    def test_validate_metric_name_with_valid_name(self):
        """Test validate_metric_name with valid metric name."""
        # Should not raise
        validate_metric_name(
            "model.predictions_total",
            model_category="internal",
            model_type="regression",
        )

    def test_validate_metric_name_with_invalid_name(self):
        """Test validate_metric_name with invalid metric name."""
        with pytest.raises(ValidationError):
            validate_metric_name(
                "custom_metric", model_category="internal", model_type="regression"
            )

    def test_validate_metric_name_default_parameters(self):
        """Test validate_metric_name with default parameters."""
        # Should use internal/regression by default
        validate_metric_name("model.predictions_total")

        with pytest.raises(ValidationError):
            validate_metric_name("genai.requests_total")

    def test_validate_metric_name_non_strict_mode(self):
        """Test validate_metric_name with strict=False."""
        # Should not raise
        validate_metric_name("custom_metric", strict=False)


class TestConstants:
    """Test module-level constants."""

    def test_metric_patterns_contains_all_prefixes(self):
        """Test METRIC_PATTERNS contains all valid prefixes."""
        assert "model" in METRIC_PATTERNS
        assert "genai" in METRIC_PATTERNS
        assert "agentic" in METRIC_PATTERNS
        assert "vendor" in METRIC_PATTERNS

    def test_valid_units_contains_expected_units(self):
        """Test VALID_UNITS contains expected units."""
        assert "total" in VALID_UNITS
        assert "seconds" in VALID_UNITS
        assert "milliseconds" in VALID_UNITS
        assert "ratio" in VALID_UNITS
        assert "count" in VALID_UNITS
        assert "dollars" in VALID_UNITS

    def test_valid_prefixes_contains_expected_prefixes(self):
        """Test VALID_PREFIXES contains expected prefixes."""
        assert "model" in VALID_PREFIXES
        assert "genai" in VALID_PREFIXES
        assert "agentic" in VALID_PREFIXES
        assert "vendor" in VALID_PREFIXES
