import pytest

from cyberbrain26.control_plane import AsyncEventBus, Event, MemoryTaskQueue, TaskStatus


@pytest.mark.asyncio
async def test_event_emission_single_subscriber():
    bus = AsyncEventBus()
    received: list[Event] = []

    async def handler(event: Event) -> None:
        received.append(event)

    bus.subscribe("test.event", handler)
    await bus.publish("test.event", {"x": 1})

    assert len(received) == 1
    assert received[0].name == "test.event"
    assert received[0].payload == {"x": 1}


@pytest.mark.asyncio
async def test_multiple_subscribers_all_called():
    bus = AsyncEventBus()
    order: list[str] = []

    async def h1(event: Event) -> str:
        order.append("h1")
        return "r1"

    async def h2(event: Event) -> str:
        order.append("h2")
        return "r2"

    bus.subscribe("e", h1)
    bus.subscribe("e", h2)
    results = await bus.publish("e", {"k": "v"})

    assert set(order) == {"h1", "h2"}
    assert len(order) == 2
    assert set(results) == {"r1", "r2"}


@pytest.mark.asyncio
async def test_handler_exception_is_isolated():
    bus = AsyncEventBus()

    async def bad(event: Event) -> None:
        raise RuntimeError("boom")

    async def good(event: Event) -> str:
        return "ok"

    bus.subscribe("e", bad)
    bus.subscribe("e", good)
    results = await bus.publish("e", {})

    assert "ok" in results


@pytest.mark.asyncio
async def test_unsubscribe_stops_delivery():
    bus = AsyncEventBus()
    received: list[Event] = []

    async def handler(event: Event) -> None:
        received.append(event)

    bus.subscribe("e", handler)
    assert bus.unsubscribe("e", handler) is True
    assert bus.subscribers("e") == 0

    await bus.publish("e", {})
    assert received == []


@pytest.mark.asyncio
async def test_routing_lists_subscribers():
    bus = AsyncEventBus()

    async def h1(event: Event) -> None: ...

    async def h2(event: Event) -> None: ...

    bus.subscribe("e", h1)
    bus.subscribe("e", h2)
    assert bus.routing("e") == [h1, h2]


@pytest.mark.asyncio
async def test_memory_task_queue_status_lifecycle():
    queue = MemoryTaskQueue()

    async def work() -> int:
        return 42

    task_id = queue.enqueue(work)
    record = queue.get(task_id)
    assert record is not None
    assert record.status is TaskStatus.PENDING

    await queue.run()

    assert record.status is TaskStatus.COMPLETED
    assert record.result == 42
    assert queue.status(task_id) is TaskStatus.COMPLETED


@pytest.mark.asyncio
async def test_memory_task_queue_failure_captured():
    queue = MemoryTaskQueue()

    async def boom() -> int:
        raise ValueError("kaboom")

    task_id = queue.enqueue(boom)
    await queue.run()

    record = queue.get(task_id)
    assert record is not None
    assert record.status is TaskStatus.FAILED
    assert isinstance(record.error, ValueError)
    assert str(record.error) == "kaboom"


@pytest.mark.asyncio
async def test_memory_task_queue_multiple_tasks():
    queue = MemoryTaskQueue(max_concurrency=2)

    async def echo(value: int) -> int:
        return value

    ids = [queue.enqueue(echo, i) for i in range(3)]
    records = [queue.get(tid) for tid in ids]
    await queue.run()

    assert all(r.status is TaskStatus.COMPLETED for r in records)
    assert [r.result for r in records] == [0, 1, 2]


@pytest.mark.asyncio
async def test_memory_task_queue_drains_newly_enqueued():
    queue = MemoryTaskQueue()
    captured: list[str] = []

    async def step_one() -> None:
        queue.enqueue(step_two)

    async def step_two() -> None:
        captured.append("two")

    queue.enqueue(step_one)
    await queue.run()

    # run() keeps draining until the queue is empty, so the task enqueued by
    # step_one is executed within the same run() call.
    assert captured == ["two"]


@pytest.mark.asyncio
async def test_memory_task_queue_retry_succeeds():
    """Task that fails twice then succeeds should complete with retries."""
    queue = MemoryTaskQueue(max_retries=3)
    attempts: list[int] = []

    async def flaky() -> str:
        attempts.append(1)
        if len(attempts) < 3:
            raise ValueError("transient")
        return "recovered"

    task_id = queue.enqueue(flaky)
    record = queue.get(task_id)
    assert record is not None

    result = await queue.dispatch(record)
    assert result == "recovered"
    assert record.status is TaskStatus.COMPLETED
    assert len(attempts) == 3
    assert record.attempts == 3


@pytest.mark.asyncio
async def test_memory_task_queue_retry_max_exhausted():
    """Task that always fails should exhaust retries and raise."""
    queue = MemoryTaskQueue(max_retries=2)
    attempts: list[int] = []

    async def always_fails() -> None:
        attempts.append(1)
        raise RuntimeError("permanent")

    task_id = queue.enqueue(always_fails)
    record = queue.get(task_id)
    assert record is not None

    with pytest.raises(RuntimeError, match="permanent"):
        await queue.dispatch(record)
    assert record.status is TaskStatus.FAILED
    assert len(attempts) == 3  # initial + 2 retries
    assert record.attempts == 3
