"""
Tests for validation error utilities.

Tests the format_validation_error() and handle_validation_error() functions
that transform Pydantic validation errors into user-friendly messages.
"""

from django.test import TestCase, tag
import json
from pydantic import BaseModel, ValidationError, field_validator
from typing import Literal

from ninja.errors import HttpError
from epapi.utils.validation_errors import format_validation_error, handle_validation_error


@tag("api", "utils")
class ValidationErrorUtilityTests(TestCase):
    """Test validation error utility functions."""

    def test_format_missing_field_error(self):
        """Test formatting of missing required field error."""

        # Create a model with required field
        class TestModel(BaseModel):
            required_field: str

        # Trigger validation error
        try:
            TestModel()
        except ValidationError as e:
            errors = e.errors()
            self.assertEqual(len(errors), 1)
            formatted = format_validation_error(errors[0])
            self.assertEqual(formatted, "This field is required")

    def test_format_enum_error(self):
        """Test formatting of enum validation error."""

        class TestModel(BaseModel):
            status: Literal["active", "inactive"]

        try:
            TestModel(status="invalid")
        except ValidationError as e:
            errors = e.errors()
            self.assertEqual(len(errors), 1)
            formatted = format_validation_error(errors[0])
            # Literal errors get formatted as "Please enter ..." with the valid options
            self.assertIn("Please enter", formatted)
            self.assertIn("active", formatted)
            self.assertIn("inactive", formatted)

    def test_format_type_errors(self):
        """Test formatting of type validation errors (string, int, float)."""
        test_cases = [
            # (field_type, invalid_value, expected_message)
            # Note: We don't check exact error_type as Pydantic may use different types
            # (e.g., int_type vs int_parsing) but we verify the formatted message is correct
            (str, 123, "Please enter a valid string"),
            (int, "not_a_number", "Please enter a valid int"),
            (float, "not_a_float", "Please enter a valid float"),
        ]

        for field_type, invalid_value, expected_message in test_cases:
            with self.subTest(field_type=field_type.__name__):

                class TestModel(BaseModel):
                    field: field_type

                try:
                    TestModel(field=invalid_value)
                except ValidationError as e:
                    errors = e.errors()
                    self.assertEqual(len(errors), 1)
                    formatted = format_validation_error(errors[0])
                    self.assertEqual(formatted, expected_message)

    def test_format_value_error(self):
        """Test formatting of value error from custom validator."""

        class TestModel(BaseModel):
            age: int

            @field_validator("age")
            @classmethod
            def validate_age(cls, v):
                if v < 0:
                    raise ValueError("Age must be positive")
                return v

        try:
            TestModel(age=-5)
        except ValidationError as e:
            errors = e.errors()
            self.assertEqual(len(errors), 1)
            formatted = format_validation_error(errors[0])
            self.assertEqual(formatted, "Age must be positive")

    def test_format_unknown_error_type_fallback(self):
        """Test that unknown error types fall back to default formatting."""
        # Mock an error with an unknown type
        mock_error = {
            "type": "unknown_custom_type",
            "msg": "Input should be a valid email address",
            "ctx": {},
        }

        formatted = format_validation_error(mock_error)
        # Should use the else branch which does replacements on the message
        self.assertEqual(formatted, "Please enter a valid email address")

    def test_handle_validation_error_structure(self):
        """Test that handle_validation_error raises HttpError with correct structure."""

        class TestModel(BaseModel):
            name: str
            count: int

        try:
            TestModel(name=123, count="invalid")
        except ValidationError as e:
            # handle_validation_error should raise HttpError
            with self.assertRaises(HttpError) as context:
                handle_validation_error(e)

            http_error = context.exception
            self.assertEqual(http_error.status_code, 400)

            # Parse the JSON from the error message
            error_data = json.loads(http_error.message)

            # Check structure
            self.assertEqual(error_data["type"], "validation_error")
            self.assertIn("field_errors", error_data)
            self.assertIn("message", error_data)
            self.assertEqual(error_data["message"], "Please correct the errors below")

            # Check that both fields have errors
            self.assertIn("name", error_data["field_errors"])
            self.assertIn("count", error_data["field_errors"])

    def test_handle_validation_error_no_pydantic_internals(self):
        """Test that handle_validation_error doesn't expose Pydantic internals."""

        class TestModel(BaseModel):
            email: str

        try:
            TestModel(email=123)
        except ValidationError as e:
            with self.assertRaises(HttpError) as context:
                handle_validation_error(e)

            http_error = context.exception
            error_data = json.loads(http_error.message)
            error_str = json.dumps(error_data)

            # Ensure no Pydantic internals are exposed
            self.assertNotIn("pydantic", error_str.lower())
            self.assertNotIn("https://errors.pydantic.dev", error_str)
            self.assertNotIn("loc", error_str)

    def test_handle_validation_error_user_friendly_messages(self):
        """Test that all error messages are user-friendly."""

        class TestModel(BaseModel):
            name: str
            age: int
            status: Literal["active", "inactive"]

        try:
            TestModel(name=123, status="invalid")  # Multiple errors
        except ValidationError as e:
            with self.assertRaises(HttpError) as context:
                handle_validation_error(e)

            http_error = context.exception
            error_data = json.loads(http_error.message)

            # All messages should be user-friendly (contain "Please" or "This field")
            for field, messages in error_data["field_errors"].items():
                for message in messages:
                    # User-friendly messages start with "Please" or "This field"
                    self.assertTrue(
                        message.startswith("Please") or message.startswith("This field"),
                        f"Message '{message}' is not user-friendly",
                    )

    def test_handle_validation_error_multiple_errors_same_field(self):
        """Test handling multiple validation errors for the same field."""

        class TestModel(BaseModel):
            value: int

            @field_validator("value")
            @classmethod
            def validate_range(cls, v):
                if v < 0:
                    raise ValueError("Must be non-negative")
                if v > 100:
                    raise ValueError("Must be at most 100")
                return v

        # Test with string (type error) - this will fail before the validator runs
        try:
            TestModel(value="invalid")
        except ValidationError as e:
            with self.assertRaises(HttpError) as context:
                handle_validation_error(e)

            http_error = context.exception
            error_data = json.loads(http_error.message)

            # Should have error for 'value' field
            self.assertIn("value", error_data["field_errors"])
            self.assertIsInstance(error_data["field_errors"]["value"], list)
            self.assertGreater(len(error_data["field_errors"]["value"]), 0)
