"""Tests for MockKernelBackend."""

import asyncio
import pytest

from skop_runner.kernel_backend import (
    KernelBackend,
    KernelInfo,
    KernelInputError,
    KernelNotFoundError,
    KernelStatus,
    get_backend,
    list_backends,
)
from skop_runner.mock_backend import MockKernelBackend


class TestProtocolCompliance:
    """Verify MockKernelBackend satisfies KernelBackend protocol."""

    def test_implements_protocol(self):
        backend = MockKernelBackend()
        assert isinstance(backend, KernelBackend)

    def test_registered_as_mock(self):
        # Importing mock_backend registers it
        assert "mock" in list_backends()

    def test_get_via_factory(self):
        backend = get_backend("mock")
        assert isinstance(backend, MockKernelBackend)


class TestLifecycle:
    """Test backend lifecycle (initialize/close/is_ready)."""

    def test_not_ready_before_initialize(self, mock_backend):
        assert mock_backend.is_ready() is False

    @pytest.mark.asyncio
    async def test_ready_after_initialize(self, mock_backend):
        await mock_backend.initialize()
        assert mock_backend.is_ready() is True

    @pytest.mark.asyncio
    async def test_not_ready_after_close(self, mock_backend):
        await mock_backend.initialize()
        await mock_backend.close()
        assert mock_backend.is_ready() is False

    @pytest.mark.asyncio
    async def test_set_root_directory(self, mock_backend):
        result = await mock_backend.set_root_directory("/tmp/project")
        assert result == "/tmp/project"


class TestKernelLifecycle:
    """Test kernel start/stop/restart/interrupt."""

    @pytest.mark.asyncio
    async def test_start_kernel(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        assert kernel_id.startswith("mock-kernel-")
        assert len(initialized_mock_backend.list_kernels()) == 1

    @pytest.mark.asyncio
    async def test_stop_kernel(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        await initialized_mock_backend.stop_kernel(kernel_id)
        assert len(initialized_mock_backend.list_kernels()) == 0

    @pytest.mark.asyncio
    async def test_stop_nonexistent_kernel(self, initialized_mock_backend):
        with pytest.raises(KernelNotFoundError):
            await initialized_mock_backend.stop_kernel("nonexistent")

    @pytest.mark.asyncio
    async def test_restart_kernel_clears_state(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()

        # Execute code to set a variable
        async for _ in initialized_mock_backend.execute_code(kernel_id, "x = 42"):
            pass

        # Restart clears namespace
        await initialized_mock_backend.restart_kernel(kernel_id)

        # Variable should be gone
        outputs = []
        async for output in initialized_mock_backend.execute_code(kernel_id, "x"):
            outputs.append(output)
        assert any(o["output_type"] == "error" for o in outputs)

    @pytest.mark.asyncio
    async def test_restart_resets_execution_count(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        async for _ in initialized_mock_backend.execute_code(kernel_id, "1"):
            pass
        assert initialized_mock_backend.get_execution_count(kernel_id) == 1

        await initialized_mock_backend.restart_kernel(kernel_id)
        assert initialized_mock_backend.get_execution_count(kernel_id) == 0

    @pytest.mark.asyncio
    async def test_shutdown_all(self, initialized_mock_backend):
        await initialized_mock_backend.start_kernel()
        await initialized_mock_backend.start_kernel()
        assert len(initialized_mock_backend.list_kernels()) == 2

        result = await initialized_mock_backend.shutdown_all()
        assert len(result["stopped"]) == 2
        assert result["failed"] == {}
        assert len(initialized_mock_backend.list_kernels()) == 0

    @pytest.mark.asyncio
    async def test_start_with_custom_params(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel(
            python_path="/usr/bin/python3.11",
            env_name="test-venv",
        )
        info = initialized_mock_backend.get_kernel_info(kernel_id)
        assert info.python_path == "/usr/bin/python3.11"
        assert info.env_name == "test-venv"

    @pytest.mark.asyncio
    async def test_multiple_kernels_independent(self, initialized_mock_backend):
        k1 = await initialized_mock_backend.start_kernel()
        k2 = await initialized_mock_backend.start_kernel()

        # Set variable in k1 only
        async for _ in initialized_mock_backend.execute_code(k1, "shared = 'k1'"):
            pass

        # k2 should not have that variable
        outputs = []
        async for output in initialized_mock_backend.execute_code(k2, "shared"):
            outputs.append(output)
        assert any(o["output_type"] == "error" for o in outputs)


class TestCodeExecution:
    """Test code execution with real Python eval/exec."""

    @pytest.mark.asyncio
    async def test_print_produces_stream(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        outputs = []
        async for output in initialized_mock_backend.execute_code(kernel_id, "print('hello')"):
            outputs.append(output)

        assert len(outputs) == 1
        assert outputs[0]["output_type"] == "stream"
        assert outputs[0]["name"] == "stdout"
        assert "hello" in outputs[0]["text"]

    @pytest.mark.asyncio
    async def test_expression_produces_result(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        outputs = []
        async for output in initialized_mock_backend.execute_code(kernel_id, "2 + 3"):
            outputs.append(output)

        assert any(
            o["output_type"] == "execute_result" and o["data"]["text/plain"] == "5"
            for o in outputs
        )

    @pytest.mark.asyncio
    async def test_none_expression_no_result(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        outputs = []
        async for output in initialized_mock_backend.execute_code(kernel_id, "None"):
            outputs.append(output)

        assert not any(o["output_type"] == "execute_result" for o in outputs)

    @pytest.mark.asyncio
    async def test_variable_persistence(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()

        # Set variable
        async for _ in initialized_mock_backend.execute_code(kernel_id, "x = 42"):
            pass

        # Read it back
        outputs = []
        async for output in initialized_mock_backend.execute_code(kernel_id, "x"):
            outputs.append(output)

        assert any(
            o["output_type"] == "execute_result" and o["data"]["text/plain"] == "42"
            for o in outputs
        )

    @pytest.mark.asyncio
    async def test_multiline_code(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        code = "a = 10\nb = 20\nprint(a + b)"
        outputs = []
        async for output in initialized_mock_backend.execute_code(kernel_id, code):
            outputs.append(output)

        assert any(
            o["output_type"] == "stream" and "30" in o["text"]
            for o in outputs
        )

    @pytest.mark.asyncio
    async def test_error_produces_error_output(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        outputs = []
        async for output in initialized_mock_backend.execute_code(kernel_id, "1 / 0"):
            outputs.append(output)

        assert len(outputs) == 1
        assert outputs[0]["output_type"] == "error"
        assert outputs[0]["ename"] == "ZeroDivisionError"
        assert len(outputs[0]["traceback"]) > 0

    @pytest.mark.asyncio
    async def test_name_error(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        outputs = []
        async for output in initialized_mock_backend.execute_code(kernel_id, "undefined_var"):
            outputs.append(output)

        assert outputs[0]["output_type"] == "error"
        assert outputs[0]["ename"] == "NameError"

    @pytest.mark.asyncio
    async def test_execution_count_increments(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()

        async for _ in initialized_mock_backend.execute_code(kernel_id, "1"):
            pass
        assert initialized_mock_backend.get_execution_count(kernel_id) == 1

        async for _ in initialized_mock_backend.execute_code(kernel_id, "2"):
            pass
        assert initialized_mock_backend.get_execution_count(kernel_id) == 2

    @pytest.mark.asyncio
    async def test_import_works(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        outputs = []
        async for output in initialized_mock_backend.execute_code(
            kernel_id, "import math\nprint(math.pi)"
        ):
            outputs.append(output)

        assert any(
            o["output_type"] == "stream" and "3.14" in o["text"]
            for o in outputs
        )


class TestInputHandling:
    """Test input() simulation."""

    @pytest.mark.asyncio
    async def test_input_request_yielded(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        received_outputs = []
        execution_done = asyncio.Event()

        async def run_execution():
            async for output in initialized_mock_backend.execute_code(
                kernel_id, "x = input('Name: ')"
            ):
                received_outputs.append(output)
            execution_done.set()

        task = asyncio.create_task(run_execution())

        # Wait for input_request
        for _ in range(100):
            await asyncio.sleep(0.05)
            if any(o["output_type"] == "input_request" for o in received_outputs):
                break

        input_requests = [o for o in received_outputs if o["output_type"] == "input_request"]
        assert len(input_requests) >= 1
        assert input_requests[0]["prompt"] == "Name: "
        assert input_requests[0]["password"] is False

        # Reply to unblock
        await initialized_mock_backend.send_input_reply(kernel_id, "Alice")
        await asyncio.wait_for(execution_done.wait(), timeout=5.0)

        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass

    @pytest.mark.asyncio
    async def test_input_value_used(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        received_outputs = []
        execution_done = asyncio.Event()

        async def run_execution():
            async for output in initialized_mock_backend.execute_code(
                kernel_id, "name = input('> ')\nprint(f'Hello {name}')"
            ):
                received_outputs.append(output)
            execution_done.set()

        task = asyncio.create_task(run_execution())

        # Wait for input_request
        for _ in range(100):
            await asyncio.sleep(0.05)
            if any(o["output_type"] == "input_request" for o in received_outputs):
                break

        await initialized_mock_backend.send_input_reply(kernel_id, "World")
        await asyncio.wait_for(execution_done.wait(), timeout=5.0)

        stream_outputs = [o for o in received_outputs if o["output_type"] == "stream"]
        all_text = "".join(o["text"] for o in stream_outputs)
        assert "Hello World" in all_text

        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass

    @pytest.mark.asyncio
    async def test_input_reply_not_awaiting(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        with pytest.raises(KernelInputError):
            await initialized_mock_backend.send_input_reply(kernel_id, "value")


class TestStatusAndDiscovery:
    """Test status/info/list methods."""

    @pytest.mark.asyncio
    async def test_kernel_status_running(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        status = await initialized_mock_backend.get_kernel_status(kernel_id)
        assert status["status"] == "running"

    @pytest.mark.asyncio
    async def test_kernel_status_not_found(self, initialized_mock_backend):
        status = await initialized_mock_backend.get_kernel_status("nonexistent")
        assert status["status"] == "not_found"

    def test_kernel_info(self, initialized_mock_backend):
        # No kernel exists
        assert initialized_mock_backend.get_kernel_info("nonexistent") is None

    @pytest.mark.asyncio
    async def test_kernel_info_exists(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel(env_name="test")
        info = initialized_mock_backend.get_kernel_info(kernel_id)
        assert isinstance(info, KernelInfo)
        assert info.kernel_id == kernel_id
        assert info.env_name == "test"

    @pytest.mark.asyncio
    async def test_connection_info(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        conn = initialized_mock_backend.get_kernel_connection_info(kernel_id)
        assert conn is not None
        assert "session_id" in conn
        assert conn["session_id"].startswith("mock-session-")

    def test_connection_info_not_found(self, initialized_mock_backend):
        assert initialized_mock_backend.get_kernel_connection_info("nonexistent") is None

    @pytest.mark.asyncio
    async def test_list_kernels(self, initialized_mock_backend):
        await initialized_mock_backend.start_kernel()
        await initialized_mock_backend.start_kernel()
        kernels = initialized_mock_backend.list_kernels()
        assert len(kernels) == 2
        assert all(k["status"] == "running" for k in kernels)

    @pytest.mark.asyncio
    async def test_list_available_kernels(self, initialized_mock_backend):
        available = await initialized_mock_backend.list_available_kernels()
        assert len(available) >= 1
        assert available[0]["language"] == "python"

    @pytest.mark.asyncio
    async def test_validate_kernel(self, initialized_mock_backend):
        kernel_id = await initialized_mock_backend.start_kernel()
        assert await initialized_mock_backend.validate_kernel(kernel_id) is True
        assert await initialized_mock_backend.validate_kernel("nonexistent") is False

    @pytest.mark.asyncio
    async def test_execution_count_not_found(self, initialized_mock_backend):
        with pytest.raises(KernelNotFoundError):
            initialized_mock_backend.get_execution_count("nonexistent")
