"""Unit tests for MCA SDK custom exception hierarchy.

Tests all exception classes for proper inheritance, message quality,
and correct exception catching behavior.
"""

import pytest
from mca_sdk.utils.exceptions import (
    MCASDKError,
    ConfigurationError,
    RegistryError,
    RegistryConnectionError,
    RegistryConfigNotFoundError,
    RegistryAuthError,
    ValidationError,
    BufferingError,
)


class TestBaseException:
    """Tests for MCASDKError base exception class."""

    def test_base_exception_exists(self):
        """Verify MCASDKError can be instantiated."""
        error = MCASDKError("test error")
        assert str(error) == "test error"

    def test_base_exception_is_exception(self):
        """Verify MCASDKError inherits from built-in Exception."""
        assert issubclass(MCASDKError, Exception)


class TestConfigurationError:
    """Tests for ConfigurationError exception (AC: 1)."""

    def test_configuration_error_inheritance(self):
        """Verify ConfigurationError inherits from MCASDKError."""
        assert issubclass(ConfigurationError, MCASDKError)

    def test_configuration_error_message(self):
        """Verify ConfigurationError accepts and stores message."""
        error = ConfigurationError("Missing service_name in configuration")
        assert "service_name" in str(error)
        assert "configuration" in str(error).lower()

    def test_configuration_error_raised(self):
        """Verify ConfigurationError can be raised and caught."""
        with pytest.raises(ConfigurationError) as exc_info:
            raise ConfigurationError("Invalid configuration")
        assert "Invalid configuration" in str(exc_info.value)

    def test_configuration_error_caught_by_base(self):
        """Verify ConfigurationError can be caught by MCASDKError."""
        with pytest.raises(MCASDKError):
            raise ConfigurationError("test")


class TestRegistryError:
    """Tests for RegistryError exception (AC: 2)."""

    def test_registry_error_inheritance(self):
        """Verify RegistryError inherits from MCASDKError."""
        assert issubclass(RegistryError, MCASDKError)

    def test_registry_error_message(self):
        """Verify RegistryError accepts and stores message."""
        error = RegistryError("Registry lookup failed for model mdl-001")
        assert "mdl-001" in str(error)
        assert "Registry" in str(error)

    def test_registry_error_raised(self):
        """Verify RegistryError can be raised and caught."""
        with pytest.raises(RegistryError) as exc_info:
            raise RegistryError("Registry error")
        assert "Registry error" in str(exc_info.value)

    def test_registry_error_caught_by_base(self):
        """Verify RegistryError can be caught by MCASDKError."""
        with pytest.raises(MCASDKError):
            raise RegistryError("test")


class TestRegistrySubExceptions:
    """Tests for RegistryError sub-exceptions."""

    def test_registry_connection_error_inheritance(self):
        """Verify RegistryConnectionError inherits from RegistryError."""
        assert issubclass(RegistryConnectionError, RegistryError)
        assert issubclass(RegistryConnectionError, MCASDKError)

    def test_registry_connection_error_message(self):
        """Verify RegistryConnectionError has contextual message."""
        error = RegistryConnectionError(
            "Failed to connect to registry at https://registry.example.com: timeout after 30s"
        )
        assert "timeout" in str(error).lower()
        assert "registry" in str(error).lower()

    def test_registry_connection_error_caught_by_registry_error(self):
        """Verify RegistryConnectionError can be caught by RegistryError."""
        with pytest.raises(RegistryError):
            raise RegistryConnectionError("Connection failed")

    def test_registry_config_not_found_error_inheritance(self):
        """Verify RegistryConfigNotFoundError inherits from RegistryError."""
        assert issubclass(RegistryConfigNotFoundError, RegistryError)
        assert issubclass(RegistryConfigNotFoundError, MCASDKError)

    def test_registry_config_not_found_error_message(self):
        """Verify RegistryConfigNotFoundError has helpful message."""
        error = RegistryConfigNotFoundError(
            "Model configuration not found for model_id='mdl-999'. "
            "Verify model_id is correct and registered."
        )
        assert "mdl-999" in str(error)
        assert "not found" in str(error).lower()

    def test_registry_config_not_found_error_caught_by_registry_error(self):
        """Verify RegistryConfigNotFoundError can be caught by RegistryError."""
        with pytest.raises(RegistryError):
            raise RegistryConfigNotFoundError("Config not found")

    def test_registry_auth_error_inheritance(self):
        """Verify RegistryAuthError inherits from RegistryError."""
        assert issubclass(RegistryAuthError, RegistryError)
        assert issubclass(RegistryAuthError, MCASDKError)

    def test_registry_auth_error_message(self):
        """Verify RegistryAuthError has contextual message."""
        error = RegistryAuthError(
            "Registry authentication failed: token expired. "
            "Please refresh your authentication token."
        )
        assert "authentication" in str(error).lower()
        assert "token" in str(error).lower()

    def test_registry_auth_error_caught_by_registry_error(self):
        """Verify RegistryAuthError can be caught by RegistryError."""
        with pytest.raises(RegistryError):
            raise RegistryAuthError("Auth failed")


class TestValidationError:
    """Tests for ValidationError exception (AC: 3)."""

    def test_validation_error_inheritance(self):
        """Verify ValidationError inherits from MCASDKError."""
        assert issubclass(ValidationError, MCASDKError)

    def test_validation_error_message(self):
        """Verify ValidationError accepts and stores message."""
        error = ValidationError(
            "Invalid metric name 'my-metric': must follow pattern ^[a-z][a-z0-9_]*$"
        )
        assert "my-metric" in str(error)
        assert "Invalid" in str(error)

    def test_validation_error_raised(self):
        """Verify ValidationError can be raised and caught."""
        with pytest.raises(ValidationError) as exc_info:
            raise ValidationError("Validation failed")
        assert "Validation failed" in str(exc_info.value)

    def test_validation_error_caught_by_base(self):
        """Verify ValidationError can be caught by MCASDKError."""
        with pytest.raises(MCASDKError):
            raise ValidationError("test")


class TestBufferingError:
    """Tests for BufferingError exception."""

    def test_buffering_error_inheritance(self):
        """Verify BufferingError inherits from MCASDKError."""
        assert issubclass(BufferingError, MCASDKError)

    def test_buffering_error_message(self):
        """Verify BufferingError accepts and stores message."""
        error = BufferingError("Queue is full: cannot accept more items (max: 1000)")
        assert "Queue" in str(error)
        assert "full" in str(error).lower()

    def test_buffering_error_raised(self):
        """Verify BufferingError can be raised and caught."""
        with pytest.raises(BufferingError) as exc_info:
            raise BufferingError("Buffering failed")
        assert "Buffering failed" in str(exc_info.value)

    def test_buffering_error_caught_by_base(self):
        """Verify BufferingError can be caught by MCASDKError."""
        with pytest.raises(MCASDKError):
            raise BufferingError("test")


class TestExceptionHierarchy:
    """Tests for overall exception hierarchy structure (AC: 5)."""

    def test_all_exceptions_inherit_from_base(self):
        """Verify all SDK exceptions inherit from MCASDKError."""
        exceptions = [
            ConfigurationError,
            RegistryError,
            RegistryConnectionError,
            RegistryConfigNotFoundError,
            RegistryAuthError,
            ValidationError,
            BufferingError,
        ]
        for exc_class in exceptions:
            assert issubclass(
                exc_class, MCASDKError
            ), f"{exc_class.__name__} does not inherit from MCASDKError"

    def test_catch_all_sdk_exceptions_with_base(self):
        """Verify MCASDKError catches all SDK exceptions (AC: 5)."""
        test_exceptions = [
            ConfigurationError("test"),
            RegistryError("test"),
            RegistryConnectionError("test"),
            RegistryConfigNotFoundError("test"),
            RegistryAuthError("test"),
            ValidationError("test"),
            BufferingError("test"),
        ]

        for exception in test_exceptions:
            with pytest.raises(MCASDKError):
                raise exception

    def test_specific_exception_catching(self):
        """Verify specific exceptions can be caught individually."""
        # ConfigurationError should not be caught as RegistryError
        with pytest.raises(ConfigurationError):
            try:
                raise ConfigurationError("test")
            except RegistryError:
                pytest.fail("ConfigurationError should not be caught by RegistryError")

        # But should be caught by MCASDKError
        with pytest.raises(MCASDKError):
            raise ConfigurationError("test")


class TestMessageQuality:
    """Tests for error message quality (AC: 4)."""

    def test_configuration_error_message_is_contextual(self):
        """Verify ConfigurationError messages provide context."""
        error = ConfigurationError(
            "Missing required field 'service_name' in configuration. "
            "Set via MCA_SERVICE_NAME environment variable, YAML config, "
            "or pass service_name= parameter to MCAClient()"
        )
        message = str(error)
        # Should mention what's wrong
        assert "Missing" in message or "required" in message.lower()
        # Should mention the field
        assert "service_name" in message
        # Should provide actionable guidance
        assert "Set via" in message or "pass" in message.lower()

    def test_registry_error_message_is_contextual(self):
        """Verify RegistryError messages provide context."""
        error = RegistryConfigNotFoundError(
            "Model 'mdl-999' not found in registry. "
            "Verify model_id is correct or register the model first."
        )
        message = str(error)
        # Should mention what's wrong
        assert "not found" in message.lower()
        # Should mention the model ID
        assert "mdl-999" in message
        # Should provide actionable guidance
        assert "Verify" in message or "register" in message.lower()

    def test_validation_error_message_is_contextual(self):
        """Verify ValidationError messages provide context."""
        error = ValidationError(
            "Metric name 'MyMetric' is invalid: must be lowercase and contain only "
            "letters, numbers, and underscores. Example: 'model_latency_seconds'"
        )
        message = str(error)
        # Should mention what's wrong
        assert "invalid" in message.lower()
        # Should mention the invalid value
        assert "MyMetric" in message
        # Should provide guidance
        assert "must" in message.lower() or "Example" in message

    def test_buffering_error_message_is_contextual(self):
        """Verify BufferingError messages provide context."""
        error = BufferingError(
            "Telemetry buffer is full (1000/1000 items). "
            "Consider increasing buffer_size or reducing export_interval."
        )
        message = str(error)
        # Should mention what's wrong
        assert "full" in message.lower() or "buffer" in message.lower()
        # Should provide actionable guidance
        assert "Consider" in message or "increase" in message.lower()

    def test_no_generic_error_messages(self):
        """Verify exceptions are not raised with generic messages."""
        # This is a design guideline test - all exceptions should have
        # contextual messages, not just "Error occurred"
        generic_messages = [
            "Error occurred",
            "Invalid input",
            "Failed",
            "Error",
        ]

        # Test that our exception classes SUPPORT contextual messages
        # (we can't prevent bad usage, but we can verify good design)
        contextual_error = ConfigurationError("Missing 'service_name' - set via env or parameter")
        message = str(contextual_error)

        # Verify message is NOT generic
        for generic in generic_messages:
            assert message != generic, f"Exception message should not be generic: '{generic}'"


class TestExceptionDocstrings:
    """Tests for exception documentation (AC: 4)."""

    def test_all_exceptions_have_docstrings(self):
        """Verify all exception classes have docstrings."""
        exceptions = [
            MCASDKError,
            ConfigurationError,
            RegistryError,
            RegistryConnectionError,
            RegistryConfigNotFoundError,
            RegistryAuthError,
            ValidationError,
            BufferingError,
        ]
        for exc_class in exceptions:
            assert exc_class.__doc__ is not None, f"{exc_class.__name__} is missing a docstring"
            assert (
                len(exc_class.__doc__.strip()) > 10
            ), f"{exc_class.__name__} docstring is too short"

    def test_exception_docstrings_include_examples(self):
        """Verify exception docstrings include usage examples."""
        # Check a few key exceptions have "Examples:" in their docstrings
        exceptions_with_examples = [
            ConfigurationError,
            RegistryError,
            ValidationError,
            BufferingError,
        ]
        for exc_class in exceptions_with_examples:
            assert (
                "Example" in exc_class.__doc__
            ), f"{exc_class.__name__} docstring should include examples"


class TestExceptionHierarchyReflection:
    """Programmatic verification of exception hierarchy using reflection (AC6 requirement).

    Uses inspect module to dynamically discover all exception classes and verify
    they inherit from MCASDKError. This prevents human error when adding new exceptions.
    """

    def test_all_error_classes_inherit_from_base(self):
        """Programmatically verify ALL exception classes inherit from MCASDKError.

        Uses reflection to discover all classes ending in 'Error' in the exceptions module.
        This ensures future exceptions also comply with the hierarchy requirement.
        """
        import inspect
        import mca_sdk.utils.exceptions as exceptions_module

        # Get all classes from the exceptions module
        all_classes = inspect.getmembers(exceptions_module, inspect.isclass)

        # Filter to classes ending in 'Error' (our exception naming convention)
        error_classes = [(name, cls) for name, cls in all_classes if name.endswith("Error")]

        # Verify we found exceptions (sanity check)
        assert len(error_classes) > 0, "No exception classes found in exceptions module"

        # Track inheritance failures
        failures = []

        for name, cls in error_classes:
            # Skip the base class itself
            if name == "MCASDKError":
                # Verify base class inherits from Exception
                assert issubclass(
                    cls, Exception
                ), "MCASDKError must inherit from built-in Exception"
                continue

            # Verify all other Error classes inherit from MCASDKError
            if not issubclass(cls, MCASDKError):
                failures.append(name)

        # Assert no failures
        assert (
            len(failures) == 0
        ), f"The following exception classes do NOT inherit from MCASDKError: {failures}"

    def test_exception_hierarchy_completeness(self):
        """Verify all exceptions documented in AC6 are present in the module."""
        import mca_sdk.utils.exceptions as exceptions_module

        # Required exceptions from AC6
        required_exceptions = [
            "MCASDKError",
            "ConfigurationError",
            "ValidationError",
            "RegistryConnectionError",
            "RegistryAuthError",
            "BufferingError",
        ]

        for exc_name in required_exceptions:
            assert hasattr(
                exceptions_module, exc_name
            ), f"Required exception '{exc_name}' not found in exceptions module"

            exc_class = getattr(exceptions_module, exc_name)
            assert isinstance(exc_class, type), f"{exc_name} is not a class"
            assert issubclass(exc_class, Exception), f"{exc_name} does not inherit from Exception"
