"""Test custom exception hierarchy implementation."""

import json
from datetime import datetime

import pytest

from augint_library.exceptions import (
    AugintError,
    ConfigurationError,
    ErrorCode,
    MultiError,
    NetworkTimeoutError,
    RateLimitError,
    ResourceAlreadyExistsError,
    ResourceNotFoundError,
    ValidationError,
    collect_errors,
    error_context,
    handle_exceptions,
)


class TestBaseException:
    """Test the base AugintError exception class."""

    def test_basic_exception_creation(self) -> None:
        """Test creating a basic exception with message."""
        error = AugintError("Something went wrong")
        assert str(error) == "Something went wrong"
        assert error.code == ErrorCode.GENERIC_ERROR
        assert error.details == {}
        assert isinstance(error.timestamp, datetime)

    def test_exception_with_code(self) -> None:
        """Test creating exception with specific error code."""
        error = AugintError("Config missing", code=ErrorCode.CONFIGURATION_ERROR)
        assert error.code == ErrorCode.CONFIGURATION_ERROR
        assert str(error) == "Config missing"

    def test_exception_with_details(self) -> None:
        """Test creating exception with additional details."""
        details = {"file": "config.yaml", "key": "database_url"}
        error = AugintError("Config error", details=details)
        assert error.details == details

    def test_exception_fluent_interface(self) -> None:
        """Test fluent interface for adding context."""
        error = (
            AugintError("Database error")
            .with_code(ErrorCode.RESOURCE_NOT_FOUND)
            .with_detail("table", "users")
            .with_detail("id", 123)
            .with_context({"operation": "fetch_user"})
        )

        assert error.code == ErrorCode.RESOURCE_NOT_FOUND
        assert error.details["table"] == "users"
        assert error.details["id"] == 123
        assert error.details["operation"] == "fetch_user"

    def test_exception_json_serialization(self) -> None:
        """Test JSON serialization of exceptions."""
        error = AugintError(
            "Test error",
            code=ErrorCode.VALIDATION_ERROR,
            details={"field": "email", "value": "invalid"},
        )

        json_data = error.to_json()
        parsed = json.loads(json_data)

        assert parsed["error"]["code"] == "VALIDATION_ERROR"
        assert parsed["error"]["message"] == "Test error"
        assert parsed["error"]["details"]["field"] == "email"
        assert "timestamp" in parsed["error"]

    def test_exception_dict_conversion(self) -> None:
        """Test converting exception to dictionary."""
        error = AugintError("Test", code=ErrorCode.NETWORK_ERROR)
        error_dict = error.to_dict()

        assert error_dict["code"] == "NETWORK_ERROR"
        assert error_dict["message"] == "Test"
        assert isinstance(error_dict["timestamp"], str)


class TestSpecializedExceptions:
    """Test specialized exception types."""

    def test_configuration_error(self) -> None:
        """Test ConfigurationError with common use cases."""
        error = ConfigurationError("Missing API key", key="api_key")
        assert error.code == ErrorCode.CONFIGURATION_ERROR
        assert error.details["key"] == "api_key"
        assert "Missing API key" in str(error)

    def test_validation_error(self) -> None:
        """Test ValidationError with field validation."""
        error = ValidationError(
            "Invalid email format", field="email", value="not-an-email", constraint="email_format"
        )
        assert error.code == ErrorCode.VALIDATION_ERROR
        assert error.details["field"] == "email"
        assert error.details["value"] == "not-an-email"
        assert error.details["constraint"] == "email_format"

    def test_resource_not_found_error(self) -> None:
        """Test ResourceNotFoundError."""
        error = ResourceNotFoundError("user", 123)
        assert error.code == ErrorCode.RESOURCE_NOT_FOUND
        assert error.details["resource_type"] == "user"
        assert error.details["resource_id"] == 123
        assert "user with id 123 not found" in str(error)

    def test_resource_already_exists_error(self) -> None:
        """Test ResourceAlreadyExistsError."""
        error = ResourceAlreadyExistsError("user", "john@example.com")
        assert error.code == ErrorCode.RESOURCE_ALREADY_EXISTS
        assert error.details["resource_type"] == "user"
        assert error.details["resource_id"] == "john@example.com"

    def test_network_timeout_error(self) -> None:
        """Test NetworkTimeoutError."""
        error = NetworkTimeoutError("api.example.com", timeout=30)
        assert error.code == ErrorCode.NETWORK_TIMEOUT
        assert error.details["service"] == "api.example.com"
        assert error.details["timeout"] == 30

    def test_rate_limit_error(self) -> None:
        """Test RateLimitError."""
        error = RateLimitError(service="github", retry_after=60, limit=100, remaining=0)
        assert error.code == ErrorCode.RATE_LIMIT_EXCEEDED
        assert error.details["service"] == "github"
        assert error.details["retry_after"] == 60
        assert error.details["limit"] == 100
        assert error.details["remaining"] == 0


class TestMultiError:
    """Test MultiError for batch operations."""

    def test_multi_error_creation(self) -> None:
        """Test creating MultiError with multiple errors."""
        errors = [
            ValidationError("Invalid name", field="name"),
            ValidationError("Invalid email", field="email"),
            ConfigurationError("Missing API key", key="api_key"),
        ]

        multi_error = MultiError(errors)
        assert len(multi_error.errors) == 3
        assert multi_error.code == ErrorCode.MULTIPLE_ERRORS
        assert "3 errors occurred" in str(multi_error)

    def test_multi_error_json_serialization(self) -> None:
        """Test JSON serialization of MultiError."""
        errors = [
            ValidationError("Invalid field", field="test"),
            NetworkTimeoutError("service.com", timeout=5),
        ]

        multi_error = MultiError(errors)
        json_data = multi_error.to_json()
        parsed = json.loads(json_data)

        assert parsed["error"]["code"] == "MULTIPLE_ERRORS"
        assert len(parsed["error"]["errors"]) == 2
        assert parsed["error"]["errors"][0]["code"] == "VALIDATION_ERROR"
        assert parsed["error"]["errors"][1]["code"] == "NETWORK_TIMEOUT"


class TestExceptionDecorators:
    """Test exception handling decorators."""

    def test_handle_exceptions_decorator(self) -> None:
        """Test @handle_exceptions decorator for exception transformation."""

        @handle_exceptions(
            {
                ValueError: ErrorCode.VALIDATION_ERROR,
                KeyError: ErrorCode.RESOURCE_NOT_FOUND,
            }
        )
        def risky_function(value: str) -> int:
            if value == "invalid":
                raise ValueError("Invalid input")
            if value == "missing":
                raise KeyError("Key not found")
            return int(value)

        # Test ValueError transformation
        with pytest.raises(AugintError) as exc_info:
            risky_function("invalid")
        assert exc_info.value.code == ErrorCode.VALIDATION_ERROR
        assert "Invalid input" in str(exc_info.value)

        # Test KeyError transformation
        with pytest.raises(AugintError) as exc_info:
            risky_function("missing")
        assert exc_info.value.code == ErrorCode.RESOURCE_NOT_FOUND

        # Test normal operation
        assert risky_function("42") == 42

    def test_collect_errors_decorator(self) -> None:
        """Test @collect_errors decorator for batch operations."""

        @collect_errors
        def validate_users(users: list[dict]) -> list[dict]:
            validated = []
            for user in users:
                if "email" not in user:
                    raise ValidationError("Missing email", field="email")
                if "@" not in user["email"]:
                    raise ValidationError("Invalid email", field="email", value=user["email"])
                validated.append(user)
            return validated

        # Test with mixed valid/invalid data
        users = [
            {"email": "valid@example.com"},
            {"name": "No Email"},
            {"email": "invalid-email"},
            {"email": "another@valid.com"},
        ]

        result, errors = validate_users(users)
        assert len(result) == 2  # Two valid users
        assert len(errors) == 2  # Two errors
        assert all(isinstance(e, ValidationError) for e in errors)

    def test_error_context_decorator(self) -> None:
        """Test @error_context decorator for automatic context addition."""

        @error_context(operation="user_creation", version="1.0")
        def create_user(email: str) -> dict:
            if "@" not in email:
                raise ValidationError("Invalid email", field="email", value=email)
            return {"id": 1, "email": email}

        # Test that context is added to exceptions
        with pytest.raises(ValidationError) as exc_info:
            create_user("invalid")

        error = exc_info.value
        assert error.details["operation"] == "user_creation"
        assert error.details["version"] == "1.0"
        assert error.details["value"] == "invalid"


class TestErrorCodeEnum:
    """Test ErrorCode enumeration."""

    def test_error_codes_have_unique_values(self) -> None:
        """Test that all error codes have unique values."""
        values = [code.value for code in ErrorCode]
        assert len(values) == len(set(values))

    def test_error_codes_are_descriptive(self) -> None:
        """Test that error codes have descriptive names."""
        expected_codes = {
            "GENERIC_ERROR",
            "CONFIGURATION_ERROR",
            "VALIDATION_ERROR",
            "RESOURCE_NOT_FOUND",
            "RESOURCE_ALREADY_EXISTS",
            "NETWORK_ERROR",
            "NETWORK_TIMEOUT",
            "RATE_LIMIT_EXCEEDED",
            "AUTHENTICATION_ERROR",
            "AUTHORIZATION_ERROR",
            "MULTIPLE_ERRORS",
        }

        actual_codes = {code.value for code in ErrorCode}
        assert expected_codes.issubset(actual_codes)


class TestExceptionTestingUtilities:
    """Test utilities for testing exceptions."""

    def test_exception_assertions(self) -> None:
        """Test that exceptions can be easily asserted in tests."""

        # Example of how library users would test their code
        def user_function():
            raise ValidationError("Bad input", field="test", value=123)

        with pytest.raises(ValidationError) as exc_info:
            user_function()

        error = exc_info.value
        assert error.code == ErrorCode.VALIDATION_ERROR
        assert error.details["field"] == "test"
        assert error.details["value"] == 123

    def test_exception_mock_generation(self) -> None:
        """Test generating mock exceptions for testing."""
        # Example of creating test exceptions
        error = ValidationError.create_mock(
            field="email", value="test@test.com", constraint="unique"
        )

        assert isinstance(error, ValidationError)
        assert error.details["field"] == "email"
        assert error.details["value"] == "test@test.com"
        assert error.details["constraint"] == "unique"
