"""S8b — poller execution:update and task event scans."""

import importlib
import json
import time

import psycopg
import pytest

import abstra_internals.environment as environment
import abstra_internals.services.db.connection as connection
import abstra_internals.services.db.migrations as migrations
import abstra_internals.services.db.poller as poller


@pytest.fixture
def db(monkeypatch, pg_uri):
    monkeypatch.setenv("ABSTRA_WEB_EDITOR_DATABASE_URI", pg_uri)
    importlib.reload(environment)
    importlib.reload(connection)
    importlib.reload(migrations)
    importlib.reload(poller)
    migrations.apply_migrations()
    yield pg_uri
    connection.close_pool()
    monkeypatch.undo()
    importlib.reload(environment)
    importlib.reload(connection)
    importlib.reload(migrations)
    importlib.reload(poller)


@pytest.fixture
def captured(monkeypatch):
    msgs = []
    monkeypatch.setattr(
        poller.BroadcastController, "broadcast", lambda *, msg: msgs.append(msg)
    )
    return msgs


def _conn(uri):
    return psycopg.connect(uri, autocommit=True)


def _now(conn, expr="now()"):
    return conn.execute(f"SELECT {expr}").fetchone()[0]


def _wait_until(predicate, timeout=5.0):
    """Poll until `predicate` holds, so a passing run costs only what it needs.

    A fixed sleep would either be slower than necessary or, on a loaded runner,
    too short to have observed what the assertion is about.
    """
    deadline = time.time() + timeout
    while time.time() < deadline:
        if predicate():
            return
        time.sleep(0.02)
    raise AssertionError(f"condition not met within {timeout}s")


def _insert_execution(conn, execution_id="ex-1"):
    conn.execute(
        "INSERT INTO executions (id, stage_id, status, pid, worker_id, context, created_at, updated_at) "
        "VALUES (%s, 's', 'running', 1, 'w', '{}'::jsonb, now(), NULL)",
        (execution_id,),
    )


def _insert_task(conn, task_id="tk-1"):
    conn.execute(
        "INSERT INTO tasks (id, type, status, target_stage_id, payload, created_at, "
        "created_by_execution, created_by_stage) "
        "VALUES (%s, 'form', 'pending', 'stage-1', '{\"k\": 1}'::jsonb, now(), 'ex-9', 'src-1')",
        (task_id,),
    )


def test_poll_executions_emits_execution_update(db, captured):
    with _conn(db) as conn:
        _insert_execution(conn)
        boot = _now(conn, "now() - interval '10 seconds'")
        poller._poll_executions(conn, boot, _now(conn), {})

    assert len(captured) == 1
    msg = json.loads(captured[0])
    assert msg == {"type": "execution:update", "payload": {"execution_id": "ex-1"}}


def test_poll_tasks_emits_task_dump(db, captured):
    with _conn(db) as conn:
        _insert_task(conn)
        boot = _now(conn, "now() - interval '10 seconds'")
        poller._poll_tasks(conn, boot, _now(conn), {})

    assert len(captured) == 1
    msg = json.loads(captured[0])
    assert msg["type"] == "task"
    payload = msg["payload"]
    # TaskDTO.dump() uses camelCase aliases — matches the frontend task handler.
    assert payload["id"] == "tk-1"
    assert payload["status"] == "pending"
    assert payload["targetStageId"] == "stage-1"
    assert payload["payload"] == {"k": 1}
    assert payload["created"]["byExecutionId"] == "ex-9"


def test_poll_executions_dedups_within_window(db, captured):
    # The row stays inside the overlap window for ~WINDOW_SECONDS/CADENCE ticks;
    # without dedup a single write produced one frame per tick.
    seen: dict = {}
    with _conn(db) as conn:
        _insert_execution(conn)
        boot = _now(conn, "now() - interval '10 seconds'")
        poller._poll_executions(conn, boot, _now(conn), seen)
        assert len(captured) == 1
        poller._poll_executions(conn, boot, _now(conn), seen)
        poller._poll_executions(conn, boot, _now(conn), seen)
    assert len(captured) == 1


def test_poll_executions_reemits_after_a_real_update(db, captured):
    # Dedup keys on (id, db_updated_at), so a genuine second write still emits.
    seen: dict = {}
    with _conn(db) as conn:
        _insert_execution(conn)
        boot = _now(conn, "now() - interval '10 seconds'")
        poller._poll_executions(conn, boot, _now(conn), seen)
        # Bump relative to the stored value (not now()) so the new timestamp is
        # deterministically distinct: two statements can share a microsecond.
        conn.execute(
            "UPDATE executions SET status='finished', "
            "db_updated_at = db_updated_at + interval '1 second' WHERE id='ex-1'"
        )
        poller._poll_executions(conn, boot, _now(conn), seen)
    assert len(captured) == 2
    assert {json.loads(m)["payload"]["execution_id"] for m in captured} == {"ex-1"}


def test_poll_executions_dedups_per_id(db, captured):
    seen: dict = {}
    with _conn(db) as conn:
        _insert_execution(conn, "ex-1")
        _insert_execution(conn, "ex-2")
        boot = _now(conn, "now() - interval '10 seconds'")
        poller._poll_executions(conn, boot, _now(conn), seen)
        poller._poll_executions(conn, boot, _now(conn), seen)
    assert len(captured) == 2
    assert {json.loads(m)["payload"]["execution_id"] for m in captured} == {
        "ex-1",
        "ex-2",
    }


def test_poll_tasks_dedups_within_window(db, captured):
    seen: dict = {}
    with _conn(db) as conn:
        _insert_task(conn)
        boot = _now(conn, "now() - interval '10 seconds'")
        poller._poll_tasks(conn, boot, _now(conn), seen)
        assert len(captured) == 1
        poller._poll_tasks(conn, boot, _now(conn), seen)
        poller._poll_tasks(conn, boot, _now(conn), seen)
    assert len(captured) == 1


def test_poll_tasks_reemits_after_a_real_update(db, captured):
    seen: dict = {}
    with _conn(db) as conn:
        _insert_task(conn)
        boot = _now(conn, "now() - interval '10 seconds'")
        poller._poll_tasks(conn, boot, _now(conn), seen)
        conn.execute(
            "UPDATE tasks SET status='completed', "
            "db_updated_at = db_updated_at + interval '1 second' WHERE id='tk-1'"
        )
        poller._poll_tasks(conn, boot, _now(conn), seen)
    assert [json.loads(m)["payload"]["status"] for m in captured] == [
        "pending",
        "completed",
    ]


def test_prune_seen_drops_tuple_keys_outside_window(db, captured):
    # The mutable-row scans key on (id, db_updated_at); _prune_seen must bound
    # those dicts the same way it bounds the log-id dict.
    seen: dict = {}
    with _conn(db) as conn:
        _insert_execution(conn)
        boot = _now(conn, "now() - interval '10 seconds'")
        poller._poll_executions(conn, boot, _now(conn), seen)
        assert len(seen) == 1
        poller._prune_seen(seen, _now(conn, "now() + interval '1 hour'"))
    assert seen == {}


def test_forward_only_ignores_pre_boot_events(db, captured):
    with _conn(db) as conn:
        _insert_execution(conn, "old")
        boot = _now(conn)  # captured after the insert
        poller._poll_executions(conn, boot, boot, {})
        poller._poll_tasks(conn, boot, boot, {})
    assert captured == []


def test_loop_threads_one_persistent_dedup_dict_per_scan(db, monkeypatch):
    # The per-scan dedup above only holds if _loop reuses the SAME dict across
    # ticks: a fresh {} per tick would restore the old behavior of one frame per
    # tick the row stays in the window. And each scan needs its OWN dict, since a
    # log id could otherwise collide with an execution id.
    #
    # Asserted on dict identity rather than by timing a real write through the
    # loop: waiting out the window would need a fixed sleep, and on a slow runner
    # that sleep can cover fewer than two ticks, passing without ever exercising
    # the re-read it is meant to catch.
    calls: dict = {"logs": [], "executions": [], "tasks": []}

    def _record(scan):
        def stub(conn, boot_cursor, last_tick, seen):
            calls[scan].append(seen)

        return stub

    for scan in calls:
        monkeypatch.setattr(poller, f"_poll_{scan}", _record(scan))

    stop, _thread = poller.start_poller()
    try:
        _wait_until(lambda: all(len(c) >= 3 for c in calls.values()))
    finally:
        stop.set()

    for scan, seen_dicts in calls.items():
        assert all(seen is seen_dicts[0] for seen in seen_dicts), (
            f"_loop passed a new dict to _poll_{scan} mid-run"
        )
    firsts = [calls[scan][0] for scan in ("logs", "executions", "tasks")]
    assert len({id(seen) for seen in firsts}) == 3, "scans share one dedup dict"
