"""Tests for kernel backend protocol compliance."""

import pytest
from typing import AsyncIterator, Dict, Any, Optional, List

from skop_runner.kernel_backend import (
    # Exceptions
    KernelBackendError,
    KernelNotFoundError,
    KernelStartError,
    KernelConnectionError,
    KernelExecutionError,
    KernelTimeoutError,
    KernelInputError,
    KernelBusyError,
    KernelResourceConflictError,
    KernelGoneError,
    # Data classes
    KernelStatus,
    KernelInfo,
    AvailableEnvironment,
    # Protocol
    KernelBackend,
    # Registry
    register_backend,
    get_backend,
    list_backends,
    unregister_backend,
)


class TestExceptionHierarchy:
    """Test that all exceptions inherit from KernelBackendError."""

    def test_kernel_not_found_error(self):
        """KernelNotFoundError should inherit from KernelBackendError."""
        assert issubclass(KernelNotFoundError, KernelBackendError)
        err = KernelNotFoundError("Kernel xyz not found")
        assert isinstance(err, KernelBackendError)

    def test_kernel_start_error(self):
        """KernelStartError should inherit from KernelBackendError."""
        assert issubclass(KernelStartError, KernelBackendError)

    def test_kernel_connection_error(self):
        """KernelConnectionError should inherit from KernelBackendError."""
        assert issubclass(KernelConnectionError, KernelBackendError)

    def test_kernel_execution_error(self):
        """KernelExecutionError should inherit from KernelBackendError."""
        assert issubclass(KernelExecutionError, KernelBackendError)

    def test_kernel_timeout_error(self):
        """KernelTimeoutError should inherit from KernelBackendError."""
        assert issubclass(KernelTimeoutError, KernelBackendError)

    def test_kernel_input_error(self):
        """KernelInputError should inherit from KernelBackendError."""
        assert issubclass(KernelInputError, KernelBackendError)

    def test_kernel_busy_error(self):
        """KernelBusyError should inherit from KernelBackendError."""
        assert issubclass(KernelBusyError, KernelBackendError)

    def test_kernel_resource_conflict_error(self):
        """KernelResourceConflictError should inherit from KernelBackendError."""
        assert issubclass(KernelResourceConflictError, KernelBackendError)

    def test_kernel_gone_error(self):
        """KernelGoneError should inherit from KernelBackendError."""
        assert issubclass(KernelGoneError, KernelBackendError)

    def test_all_exceptions_catchable_as_base(self):
        """All backend exceptions should be catchable as KernelBackendError."""
        exceptions = [
            KernelNotFoundError("test"),
            KernelStartError("test"),
            KernelConnectionError("test"),
            KernelExecutionError("test"),
            KernelTimeoutError("test"),
            KernelInputError("test"),
            KernelBusyError("test"),
            KernelResourceConflictError("test"),
            KernelGoneError("test"),
        ]
        for exc in exceptions:
            try:
                raise exc
            except KernelBackendError:
                pass  # Should catch all


class TestKernelStatus:
    """Test KernelStatus enum."""

    def test_status_values(self):
        """KernelStatus should have expected string values."""
        assert KernelStatus.STARTING.value == "starting"
        assert KernelStatus.IDLE.value == "idle"
        assert KernelStatus.BUSY.value == "busy"
        assert KernelStatus.RESTARTING.value == "restarting"
        assert KernelStatus.DEAD.value == "dead"
        assert KernelStatus.NOT_FOUND.value == "not_found"
        assert KernelStatus.UNKNOWN.value == "unknown"

    def test_status_is_string_enum(self):
        """KernelStatus values should be usable as strings."""
        assert KernelStatus.IDLE == "idle"
        assert str(KernelStatus.BUSY) == "KernelStatus.BUSY"
        assert KernelStatus.IDLE.value == "idle"


class TestKernelInfo:
    """Test KernelInfo dataclass."""

    def test_minimal_creation(self):
        """KernelInfo should work with just kernel_id."""
        info = KernelInfo(kernel_id="test-123")
        assert info.kernel_id == "test-123"
        assert info.status == KernelStatus.UNKNOWN
        assert info.python_path is None
        assert info.env_name is None
        assert info.execution_count == 0
        assert info.metadata == {}

    def test_full_creation(self):
        """KernelInfo should accept all fields."""
        info = KernelInfo(
            kernel_id="kernel-abc",
            status=KernelStatus.IDLE,
            python_path="/usr/bin/python3",
            env_name="My Venv",
            execution_count=5,
            metadata={"session_id": "sess-123"},
        )
        assert info.kernel_id == "kernel-abc"
        assert info.status == KernelStatus.IDLE
        assert info.python_path == "/usr/bin/python3"
        assert info.env_name == "My Venv"
        assert info.execution_count == 5
        assert info.metadata == {"session_id": "sess-123"}

    def test_repr(self):
        """KernelInfo should have a useful repr."""
        info = KernelInfo(
            kernel_id="k123",
            status=KernelStatus.BUSY,
            env_name="test-env",
        )
        repr_str = repr(info)
        assert "k123" in repr_str
        assert "busy" in repr_str
        assert "test-env" in repr_str


class TestAvailableEnvironment:
    """Test AvailableEnvironment dataclass."""

    def test_minimal_creation(self):
        """AvailableEnvironment should work with required fields only."""
        env = AvailableEnvironment(
            name="python3",
            display_name="Python 3",
            env_type="system",
        )
        assert env.name == "python3"
        assert env.display_name == "Python 3"
        assert env.env_type == "system"
        assert env.python_path is None
        assert env.has_ipykernel is False
        assert env.language == "python"

    def test_full_creation(self):
        """AvailableEnvironment should accept all fields."""
        env = AvailableEnvironment(
            name=".venv",
            display_name="Python 3.11.5 (.venv)",
            env_type="venv",
            python_path="/project/.venv/bin/python",
            python_version="3.11.5",
            has_ipykernel=True,
            kernel_name=None,
            language="python",
            is_active=True,
            path="/project/.venv",
        )
        assert env.name == ".venv"
        assert env.python_version == "3.11.5"
        assert env.has_ipykernel is True
        assert env.is_active is True
        assert env.path == "/project/.venv"

    def test_repr(self):
        """AvailableEnvironment should have a useful repr."""
        env = AvailableEnvironment(
            name="conda-base",
            display_name="Conda Base",
            env_type="conda",
            python_version="3.10.0",
        )
        repr_str = repr(env)
        assert "conda-base" in repr_str
        assert "conda" in repr_str
        assert "3.10.0" in repr_str


class TestKernelBackendProtocol:
    """Test that the Protocol is well-defined."""

    def test_protocol_is_runtime_checkable(self):
        """KernelBackend should be runtime checkable."""
        # runtime_checkable protocols have this attribute
        assert hasattr(KernelBackend, "__protocol_attrs__")

    def test_protocol_has_lifecycle_methods(self):
        """Protocol should define kernel lifecycle methods."""
        lifecycle_methods = [
            "start_kernel",
            "stop_kernel",
            "restart_kernel",
            "interrupt_kernel",
            "shutdown_all",
        ]
        for method in lifecycle_methods:
            assert hasattr(KernelBackend, method), f"Missing method: {method}"

    def test_protocol_has_execution_methods(self):
        """Protocol should define code execution methods."""
        execution_methods = ["execute_code", "send_input_reply"]
        for method in execution_methods:
            assert hasattr(KernelBackend, method), f"Missing method: {method}"

    def test_protocol_has_status_methods(self):
        """Protocol should define status and discovery methods."""
        status_methods = [
            "get_kernel_status",
            "get_kernel_info",
            "get_kernel_connection_info",
            "list_kernels",
            "list_available_kernels",
            "get_execution_count",
            "validate_kernel",
        ]
        for method in status_methods:
            assert hasattr(KernelBackend, method), f"Missing method: {method}"

    def test_protocol_has_state_methods(self):
        """Protocol should define backend state methods."""
        state_methods = ["is_ready", "set_root_directory"]
        for method in state_methods:
            assert hasattr(KernelBackend, method), f"Missing method: {method}"

    def test_protocol_has_lifecycle_hooks(self):
        """Protocol should define lifecycle hooks."""
        hooks = ["initialize", "close"]
        for method in hooks:
            assert hasattr(KernelBackend, method), f"Missing method: {method}"


class TestBackendRegistry:
    """Test backend registration and factory."""

    def setup_method(self):
        """Clean up registry before each test."""
        # Remove any test backends
        unregister_backend("test_dummy")
        unregister_backend("test_another")

    def teardown_method(self):
        """Clean up registry after each test."""
        unregister_backend("test_dummy")
        unregister_backend("test_another")

    def test_register_and_get_backend(self):
        """Should be able to register and retrieve a backend."""

        class DummyBackend:
            def __init__(self, **kwargs):
                self.kwargs = kwargs

        register_backend("test_dummy", DummyBackend)
        assert "test_dummy" in list_backends()

        backend = get_backend("test_dummy", foo="bar", baz=123)
        assert isinstance(backend, DummyBackend)
        assert backend.kwargs == {"foo": "bar", "baz": 123}

    def test_register_requires_class(self):
        """register_backend should reject non-classes."""
        with pytest.raises(TypeError) as exc_info:
            register_backend("invalid", "not a class")
        assert "must be a class" in str(exc_info.value)

        with pytest.raises(TypeError):
            register_backend("invalid", lambda: None)

    def test_get_unknown_backend_raises(self):
        """get_backend should raise ValueError for unknown backends."""
        with pytest.raises(ValueError) as exc_info:
            get_backend("nonexistent_backend_xyz")
        assert "Unknown backend" in str(exc_info.value)
        assert "nonexistent_backend_xyz" in str(exc_info.value)

    def test_list_backends(self):
        """list_backends should return registered backend names."""

        class Backend1:
            pass

        class Backend2:
            pass

        register_backend("test_dummy", Backend1)
        register_backend("test_another", Backend2)

        backends = list_backends()
        assert "test_dummy" in backends
        assert "test_another" in backends

    def test_unregister_backend(self):
        """unregister_backend should remove a registered backend."""

        class DummyBackend:
            pass

        register_backend("test_dummy", DummyBackend)
        assert "test_dummy" in list_backends()

        result = unregister_backend("test_dummy")
        assert result is True
        assert "test_dummy" not in list_backends()

    def test_unregister_nonexistent_backend(self):
        """unregister_backend should return False for unknown backends."""
        result = unregister_backend("never_registered_xyz")
        assert result is False

    def test_register_overwrites_existing(self):
        """Registering same name twice should overwrite."""

        class Backend1:
            name = "first"

        class Backend2:
            name = "second"

        register_backend("test_dummy", Backend1)
        register_backend("test_dummy", Backend2)

        backend = get_backend("test_dummy")
        assert backend.name == "second"


class TestProtocolCompliance:
    """Test that a minimal implementation satisfies the protocol."""

    def test_isinstance_check_with_compliant_class(self):
        """A class implementing all methods should pass isinstance check."""

        class CompliantBackend:
            async def start_kernel(
                self,
                kernel_name: str = "python3",
                python_path: Optional[str] = None,
                env_name: Optional[str] = None,
                cwd: Optional[str] = None,
            ) -> str:
                return "kernel-id"

            async def stop_kernel(self, kernel_id: str) -> None:
                pass

            async def restart_kernel(self, kernel_id: str) -> None:
                pass

            async def interrupt_kernel(self, kernel_id: str) -> None:
                pass

            async def shutdown_all(self, timeout: float = 5.0) -> Dict[str, Any]:
                return {"stopped": [], "failed": {}}

            async def execute_code(
                self, kernel_id: str, code: str, timeout: float = 300.0
            ) -> AsyncIterator[dict]:
                yield {"output_type": "stream", "name": "stdout", "text": "hello"}

            async def send_input_reply(self, kernel_id: str, value: str) -> None:
                pass

            async def get_kernel_status(self, kernel_id: str) -> Dict[str, Any]:
                return {"status": "running", "kernel_id": kernel_id}

            def get_kernel_info(self, kernel_id: str) -> Optional[KernelInfo]:
                return None

            def get_kernel_connection_info(
                self, kernel_id: str
            ) -> Optional[Dict[str, Any]]:
                return None

            def list_kernels(self) -> List[Dict[str, Any]]:
                return []

            async def list_available_kernels(self) -> List[Dict[str, Any]]:
                return []

            def get_execution_count(self, kernel_id: str) -> int:
                return 0

            async def validate_kernel(self, kernel_id: str) -> bool:
                return True

            def is_ready(self) -> bool:
                return True

            async def set_root_directory(self, path: str) -> str:
                return path

            async def initialize(self, **kwargs: Any) -> None:
                pass

            async def close(self) -> None:
                pass

        backend = CompliantBackend()
        # Note: runtime_checkable only checks method existence, not signatures
        assert isinstance(backend, KernelBackend)
