"""Tests for LISTEN/NOTIFY reactive event processing."""

from __future__ import annotations

from unittest.mock import AsyncMock, patch
from types import SimpleNamespace

import pytest

from logos_adk.event_bus import InMemoryEventBus, PostgreSQLEventBus


# ── PostgreSQLEventBus: notify_channel ───────────────────────


def test_notify_channel_default():
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db")
    assert bus.notify_channel == "events_notify"


def test_notify_channel_custom_table():
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db", table_name="my_events")
    assert bus.notify_channel == "my_events_notify"


# ── PostgreSQLEventBus: publish sends NOTIFY ─────────────────


def test_publish_sends_notify_after_insert(monkeypatch):
    """Verify that publish() executes NOTIFY after INSERT."""
    executed_queries = []

    class FakeCursor:
        rowcount = 1

        def execute(self, query, params=None):
            normalized = " ".join(str(query).split())
            executed_queries.append((normalized, params))

        def __enter__(self):
            return self

        def __exit__(self, *args):
            pass

    class FakeConn:
        def cursor(self):
            return FakeCursor()

        def commit(self):
            pass

        def __enter__(self):
            return self

        def __exit__(self, *args):
            pass

    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db")
    bus._connect = lambda: FakeConn()
    monkeypatch.setattr(PostgreSQLEventBus, "_ensure_schema", lambda self, conn: None)

    bus.publish("test.topic", {"key": "value"})

    # Should have INSERT + NOTIFY
    assert len(executed_queries) == 2
    insert_q = executed_queries[0][0]
    notify_q = executed_queries[1][0]
    assert insert_q.startswith("INSERT INTO events (")
    assert "NOTIFY events_notify, 'test.topic'" in notify_q


# ── PostgreSQLEventBus: wait_for_notify ──────────────────────


@pytest.mark.asyncio
async def test_wait_for_notify_returns_topic_on_notification():
    """Simulate a NOTIFY arriving."""
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db")

    fake_notify = SimpleNamespace(payload="crm.deal_updated")

    async def fake_notifies(timeout=5.0):
        yield fake_notify

    mock_conn = AsyncMock()
    mock_conn.closed = False
    mock_conn.notifies = fake_notifies

    bus._async_listen_conn = mock_conn

    result = await bus.wait_for_notify(timeout=1.0)
    assert result == "crm.deal_updated"


@pytest.mark.asyncio
async def test_wait_for_notify_returns_none_on_timeout():
    """When no notification arrives within timeout."""
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db")

    async def empty_notifies(timeout=5.0):
        return
        yield  # make it an async generator

    mock_conn = AsyncMock()
    mock_conn.closed = False
    mock_conn.notifies = empty_notifies

    bus._async_listen_conn = mock_conn

    result = await bus.wait_for_notify(timeout=0.01)
    assert result is None


@pytest.mark.asyncio
async def test_wait_for_notify_handles_broken_connection():
    """Broken connection should return None and discard the connection."""
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db")

    async def broken_notifies(timeout=5.0):
        raise ConnectionError("peer reset")
        yield  # make it an async generator

    mock_conn = AsyncMock()
    mock_conn.closed = False
    mock_conn.notifies = broken_notifies

    bus._async_listen_conn = mock_conn

    result = await bus.wait_for_notify(timeout=0.1)
    assert result is None
    # Connection should be discarded
    assert bus._async_listen_conn is None


@pytest.mark.asyncio
async def test_close_listen():
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db")
    mock_conn = AsyncMock()
    bus._async_listen_conn = mock_conn

    await bus.close_listen()

    mock_conn.close.assert_awaited_once()
    assert bus._async_listen_conn is None


@pytest.mark.asyncio
async def test_close_listen_when_no_connection():
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db")
    await bus.close_listen()  # Should not raise


# ── Swarm: drain_reactive ────────────────────────────────────


@pytest.mark.asyncio
async def test_swarm_drain_reactive_with_postgres_bus():
    """drain_reactive should call wait_for_notify then drain."""
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/db")

    from logos_adk.swarm import Swarm

    swarm = Swarm("test-swarm", event_bus=bus)

    with (
        patch.object(
            bus, "wait_for_notify", new_callable=AsyncMock, return_value="test.topic"
        ) as mock_wait,
        patch.object(swarm, "drain", new_callable=AsyncMock, return_value=3) as mock_drain,
    ):
        result = await swarm.drain_reactive(timeout=2.0, max_events=10)

    mock_wait.assert_awaited_once_with(timeout=2.0)
    mock_drain.assert_awaited_once_with(max_events=10)
    assert result == 3


@pytest.mark.asyncio
async def test_swarm_drain_reactive_with_inmemory_bus():
    """drain_reactive with non-PostgreSQL bus should fall back to sleep."""
    bus = InMemoryEventBus()

    from logos_adk.swarm import Swarm

    swarm = Swarm("test-swarm", event_bus=bus)

    processed_events = []

    async def handler(event):
        processed_events.append(event)
        return {"ok": True}

    swarm.subscribe("test.topic", handler)
    bus.publish("test.topic", {"data": "hello"})

    with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
        result = await swarm.drain_reactive(timeout=0.01, max_events=5)

    mock_sleep.assert_awaited_once_with(0.01)
    assert result == 1
    assert len(processed_events) == 1


@pytest.mark.asyncio
async def test_swarm_drain_reactive_processes_events_after_notify():
    """End-to-end: notify wake-up → drain processes queued event."""
    bus = InMemoryEventBus()

    from logos_adk.swarm import Swarm

    swarm = Swarm("test-swarm", event_bus=bus)

    results = []

    async def handler(event):
        results.append(event.payload)
        return {"processed": True}

    swarm.subscribe("my.topic", handler)

    # Publish an event
    bus.publish("my.topic", {"msg": "hello"})

    # drain_reactive with InMemory falls back to sleep
    with patch("asyncio.sleep", new_callable=AsyncMock):
        count = await swarm.drain_reactive(timeout=0.01, max_events=5)

    assert count == 1
    assert results[0]["msg"] == "hello"


# ── Backward Compatibility ───────────────────────────────────


@pytest.mark.asyncio
async def test_drain_still_works_unchanged():
    """Original drain() method still works as before."""
    bus = InMemoryEventBus()

    from logos_adk.swarm import Swarm

    swarm = Swarm("compat-swarm", event_bus=bus)

    async def handler(event):
        return {"done": True}

    swarm.subscribe("compat.topic", handler)
    bus.publish("compat.topic", {"data": "test"})

    count = await swarm.drain(max_events=5)
    assert count == 1

    event = bus.list_events(topic="compat.topic")[0]
    assert event.status == "completed"
