"""Tests for bus modules: publisher, subscriber, and queue."""

from __future__ import annotations

import asyncio
import json

import pytest

from loom.bus import channels
from loom.bus.events import EventType
from loom.bus.publisher import publish_event, publish_escalation, send_agent_message
from loom.bus.queue import pop_escalation, push_escalation
from loom.bus.subscriber import subscribe_events

PID = "test-proj-id"


# ---------------------------------------------------------------------------
# Publisher tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_publish_event_writes_to_stream_and_returns_id(redis_conn):
    stream_id = await publish_event(redis_conn, PID, EventType.TASK_CREATED)

    assert stream_id is not None
    # Verify the stream has an entry
    entries = await redis_conn.xrange(channels.event_stream(PID))
    assert len(entries) == 1
    entry_id, fields = entries[0]
    assert entry_id == stream_id
    assert fields["event_type"] == EventType.TASK_CREATED.value


@pytest.mark.asyncio
async def test_publish_event_with_task_id_and_payload(redis_conn):
    payload = {"key": "value", "num": 42}
    stream_id = await publish_event(
        redis_conn,
        PID,
        EventType.TASK_DONE,
        task_id="task-123",
        payload=payload,
    )

    entries = await redis_conn.xrange(channels.event_stream(PID))
    assert len(entries) == 1
    _, fields = entries[0]
    assert fields["event_type"] == EventType.TASK_DONE.value
    assert fields["task_id"] == "task-123"
    assert json.loads(fields["payload"]) == payload


@pytest.mark.asyncio
async def test_publish_escalation_pushes_to_queue_and_publishes(redis_conn):
    # Subscribe to escalation channel to verify publish
    pubsub = redis_conn.pubsub()
    await pubsub.subscribe(channels.escalation_channel(PID))
    # Drain the subscription confirmation message
    await pubsub.get_message(timeout=1)

    await publish_escalation(redis_conn, PID, "task-abc", "Something went wrong")

    # Verify queue has the item
    queue_key = channels.escalation_queue(PID)
    length = await redis_conn.llen(queue_key)
    assert length == 1
    raw = await redis_conn.rpop(queue_key)
    data = json.loads(raw)
    assert data["task_id"] == "task-abc"
    assert data["message"] == "Something went wrong"

    # Verify pub/sub message was sent
    msg = await pubsub.get_message(timeout=2)
    assert msg is not None
    assert msg["type"] == "message"
    pub_data = json.loads(msg["data"])
    assert pub_data["task_id"] == "task-abc"

    await pubsub.unsubscribe()
    await pubsub.aclose()


@pytest.mark.asyncio
async def test_send_agent_message_to_specific_agent(redis_conn):
    agent_id = "agent-007"
    pubsub = redis_conn.pubsub()
    await pubsub.subscribe(channels.agent_channel(PID, agent_id))
    await pubsub.get_message(timeout=1)  # drain subscribe confirmation

    await send_agent_message(redis_conn, PID, agent_id, "Hello agent")

    msg = await pubsub.get_message(timeout=2)
    assert msg is not None
    assert msg["type"] == "message"
    data = json.loads(msg["data"])
    assert data["to"] == agent_id
    assert data["message"] == "Hello agent"

    await pubsub.unsubscribe()
    await pubsub.aclose()


@pytest.mark.asyncio
async def test_send_agent_message_broadcast(redis_conn):
    pubsub = redis_conn.pubsub()
    await pubsub.subscribe(channels.broadcast_channel(PID))
    await pubsub.get_message(timeout=1)  # drain subscribe confirmation

    await send_agent_message(redis_conn, PID, "broadcast", "All hands")

    msg = await pubsub.get_message(timeout=2)
    assert msg is not None
    assert msg["type"] == "message"
    data = json.loads(msg["data"])
    assert data["to"] == "broadcast"
    assert data["message"] == "All hands"

    await pubsub.unsubscribe()
    await pubsub.aclose()


# ---------------------------------------------------------------------------
# Queue tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_push_then_pop_returns_correct_data(redis_conn):
    await push_escalation(redis_conn, PID, "task-1", "Escalation message")
    result = await pop_escalation(redis_conn, PID, timeout=1)

    assert result is not None
    assert result["task_id"] == "task-1"
    assert result["message"] == "Escalation message"


@pytest.mark.asyncio
async def test_pop_empty_queue_returns_none(redis_conn):
    result = await pop_escalation(redis_conn, PID, timeout=1)
    assert result is None


@pytest.mark.asyncio
async def test_fifo_order_preserved(redis_conn):
    await push_escalation(redis_conn, PID, "task-1", "First")
    await push_escalation(redis_conn, PID, "task-2", "Second")
    await push_escalation(redis_conn, PID, "task-3", "Third")

    r1 = await pop_escalation(redis_conn, PID, timeout=1)
    r2 = await pop_escalation(redis_conn, PID, timeout=1)
    r3 = await pop_escalation(redis_conn, PID, timeout=1)

    assert r1["task_id"] == "task-1"
    assert r1["message"] == "First"
    assert r2["task_id"] == "task-2"
    assert r2["message"] == "Second"
    assert r3["task_id"] == "task-3"
    assert r3["message"] == "Third"


# ---------------------------------------------------------------------------
# Subscriber test
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_subscribe_receives_events(redis_conn):
    received = []

    async def listener():
        async for event in subscribe_events(redis_conn, PID):
            received.append(event)
            if len(received) >= 1:
                break

    task = asyncio.create_task(listener())
    await asyncio.sleep(0.1)  # let subscriber attach
    await publish_event(redis_conn, PID, EventType.TASK_CREATED, task_id="t1")
    await asyncio.wait_for(task, timeout=2.0)

    assert len(received) == 1
    assert received[0]["event_type"] == EventType.TASK_CREATED.value
    assert received[0]["task_id"] == "t1"
