"""Tests for the persistent event bus."""
from __future__ import annotations

from logos_adk.event_bus import EVENT_BUS_COLUMNS, PostgreSQLEventBus, SQLiteEventBus


def test_sqlite_event_bus_publish_claim_ack_survives_restart(tmp_path):
    db_path = tmp_path / "event_bus.db"
    bus_before = SQLiteEventBus(str(db_path))

    published = bus_before.publish(
        "need_seo",
        {"prompt": "optimize title"},
        correlation_id="corr-1",
        run_id="run-1",
        publisher="graph",
    )
    assert published.status == "queued"

    bus_after = SQLiteEventBus(str(db_path))
    claimed = bus_after.claim("worker:seo", ["need_seo"], correlation_id="corr-1", run_id="run-1")
    assert claimed is not None
    assert claimed.payload == {"prompt": "optimize title"}
    assert claimed.status == "claimed"
    assert claimed.attempt == 1

    bus_after.ack(claimed.id, "worker:seo")
    stored = bus_after.get_event(claimed.id)
    assert stored is not None
    assert stored.status == "completed"
    assert stored.lease_owner is None


def test_sqlite_event_bus_requeues_claimed_event(tmp_path):
    db_path = tmp_path / "event_bus.db"
    bus = SQLiteEventBus(str(db_path))

    event = bus.publish("need_review", {"document": "draft"})
    claimed = bus.claim("worker:review", ["need_review"])
    assert claimed is not None
    assert claimed.id == event.id

    bus.requeue(event.id, "worker:review", error="temporary failure")
    requeued = bus.get_event(event.id)
    assert requeued is not None
    assert requeued.status == "queued"
    assert requeued.last_error == "temporary failure"


def test_sqlite_event_bus_lists_campaign_events(tmp_path):
    db_path = tmp_path / "event_bus.db"
    bus = SQLiteEventBus(str(db_path))

    bus.publish("need_seo", {"prompt": "optimize title"}, campaign_id="cmp-1")
    bus.publish("need_review", {"prompt": "review draft"}, campaign_id="cmp-2")

    events = bus.list_events(campaign_id="cmp-1")

    assert len(events) == 1
    assert events[0].campaign_id == "cmp-1"


class _FakePostgresCursor:
    def __init__(self, storage):
        self.storage = storage
        self.rowcount = 0
        self._fetchone_result = None
        self._fetchall_result = []

    def execute(self, query, params=None):
        normalized = " ".join(str(query).split())
        self.rowcount = 0
        self._fetchone_result = None
        self._fetchall_result = []
        events = self.storage.setdefault("events", {})

        if normalized.startswith("INSERT INTO events ("):
            event = dict(zip(EVENT_BUS_COLUMNS, params, strict=False))
            events[event["id"]] = event
            self.rowcount = 1
            return

        if "NOTIFY" in normalized:
            return  # No-op for fake backend

        if normalized.startswith("SELECT id, topic, payload_json"):
            if "WHERE id = %s" in normalized:
                event = events.get(params[0])
                self._fetchone_result = self._as_tuple(event) if event is not None else None
                return
            if "ORDER BY available_at ASC" in normalized:
                topics = set(params[0])
                now = params[1]
                correlation_id = params[3] if len(params) > 3 else None
                run_id = params[4] if len(params) > 4 else None
                candidates = [
                    event
                    for event in events.values()
                    if event["topic"] in topics
                    and (
                        (event["status"] == "queued" and event["available_at"] <= now)
                        or (
                            event["status"] == "claimed"
                            and event["lease_expires_at"] is not None
                            and event["lease_expires_at"] <= now
                        )
                    )
                    and (correlation_id is None or event["correlation_id"] == correlation_id)
                    and (run_id is None or event["run_id"] == run_id)
                ]
                candidates.sort(key=lambda item: (item["available_at"], item["created_at"], item["id"]))
                self._fetchone_result = self._as_tuple(candidates[0]) if candidates else None
                return
            filtered = list(events.values())
            param_index = 0
            if "WHERE topic = %s" in normalized:
                filtered = [event for event in filtered if event["topic"] == params[param_index]]
                param_index += 1
            if "status = %s" in normalized:
                filtered = [event for event in filtered if event["status"] == params[param_index]]
                param_index += 1
            if "campaign_id = %s" in normalized:
                filtered = [event for event in filtered if event["campaign_id"] == params[param_index]]
                param_index += 1
            if "correlation_id = %s" in normalized:
                filtered = [event for event in filtered if event["correlation_id"] == params[param_index]]
                param_index += 1
            if "run_id = %s" in normalized:
                filtered = [event for event in filtered if event["run_id"] == params[param_index]]
            filtered.sort(key=lambda item: (item["created_at"], item["id"]))
            self._fetchall_result = [self._as_tuple(event) for event in filtered]
            return

        if normalized.startswith("UPDATE events AS t SET status = 'claimed'"):
            # Atomic claim: params = [now, consumer, lease_expires_at, topics, now, now, ?corr, ?run]
            updated_at = params[0]
            lease_owner = params[1]
            lease_expires_at_val = params[2]
            topics = set(params[3])
            now = params[4]
            correlation_id = params[6] if len(params) > 6 else None
            run_id = params[7] if len(params) > 7 else None

            candidates = [
                event
                for event in events.values()
                if event["topic"] in topics
                and (
                    (event["status"] == "queued" and event["available_at"] <= now)
                    or (
                        event["status"] == "claimed"
                        and event["lease_expires_at"] is not None
                        and event["lease_expires_at"] <= now
                    )
                )
                and (correlation_id is None or event["correlation_id"] == correlation_id)
                and (run_id is None or event["run_id"] == run_id)
            ]
            candidates.sort(key=lambda item: (item["available_at"], item["created_at"], item["id"]))
            if not candidates:
                return
            event = candidates[0]
            event["status"] = "claimed"
            event["updated_at"] = updated_at
            event["lease_owner"] = lease_owner
            event["lease_expires_at"] = lease_expires_at_val
            event["attempt"] = int(event["attempt"]) + 1
            event["last_error"] = None
            self.rowcount = 1
            self._fetchone_result = self._as_tuple(event)
            return

        if normalized.startswith("UPDATE events SET status = 'completed'"):
            event = events.get(params[1])
            if event is None:
                return
            event["status"] = "completed"
            event["updated_at"] = params[0]
            event["lease_owner"] = None
            event["lease_expires_at"] = None
            self.rowcount = 1
            return

        if normalized.startswith("UPDATE events SET status = 'queued'"):
            event = events.get(params[3])
            if event is None:
                return
            event["status"] = "queued"
            event["updated_at"] = params[0]
            event["available_at"] = params[1]
            event["lease_owner"] = None
            event["lease_expires_at"] = None
            event["last_error"] = params[2]
            self.rowcount = 1
            return

    def fetchone(self):
        return self._fetchone_result

    def fetchall(self):
        return self._fetchall_result

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False

    @staticmethod
    def _as_tuple(event):
        return tuple(event[column] for column in EVENT_BUS_COLUMNS)


class _FakePostgresConnection:
    def __init__(self):
        self.storage = {"events": {}}
        self.commits = 0

    def cursor(self):
        return _FakePostgresCursor(self.storage)

    def commit(self):
        self.commits += 1

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False


def test_postgresql_event_bus_constructor_resolves_env_dsn(monkeypatch):
    monkeypatch.setenv("DATABASE_URL", "postgresql://env/event-bus")

    bus = PostgreSQLEventBus()

    assert bus.dsn == "postgresql://env/event-bus"


def test_postgresql_event_bus_publish_claim_ack_with_fake_backend(monkeypatch):
    connection = _FakePostgresConnection()
    bus = PostgreSQLEventBus("postgresql://user:pass@localhost/logos")
    bus._connect = lambda: connection
    monkeypatch.setattr(PostgreSQLEventBus, "_ensure_schema", lambda self, conn: None)

    published = bus.publish(
        "need_seo",
        {"prompt": "optimize title"},
        campaign_id="cmp-1",
        correlation_id="corr-1",
        run_id="run-1",
        publisher="swarm",
    )
    claimed = bus.claim("worker:seo", ["need_seo"], correlation_id="corr-1", run_id="run-1")

    assert published.status == "queued"
    assert claimed is not None
    assert claimed.id == published.id
    assert claimed.status == "claimed"
    assert claimed.attempt == 1

    bus.ack(claimed.id, "worker:seo")
    stored = bus.get_event(claimed.id)
    listed = bus.list_events(campaign_id="cmp-1")

    assert stored is not None
    assert stored.status == "completed"
    assert stored.lease_owner is None
    assert len(listed) == 1
    assert listed[0].payload == {"prompt": "optimize title"}
