"""Spec: primary/secondary lifecycle pod naporem (extended scenarios).

## Logika systemu

Patrz ``test_primary_secondary.py`` — to są te same role i te same reguły
frontier/transition, ale testowane w kombinacji: wiele pubkeyów, wiele
eventów, role flipy w trakcie, rescans. Każdy test miesza co najmniej
dwie reguły, a asercja końcowa weryfikuje że stan systemu jest spójny ze
spec'em (forward expansion zamrożone w czasie ingestu, role transitions
przeżywają się nawzajem, eventy przeżywają flipy role).

Sentinel: ostatni event w sekwencji jest sync pointem — po tym jak ląduje
w bazie, każdy wcześniejszy event też już przeszedł przez ingest+frontier
(serialized pod ``_sql_lock``). Patrz ``README.md``.
"""

from __future__ import annotations

from ._helpers import DbHelper, new_keypair, new_pub, signed_event


def _snapshot_roles(core) -> dict[str, str]:
    """All tracked pubkeys as ``{hex: role}``."""
    return {
        row["hex"]: row["role"]
        for row in core.run_sql("SELECT hex, role FROM tracked_pubkeys")
    }


def test_frontier_state_survives_primary_oscillation(running_core, mock_relay):
    """Po cyklu primary → secondary → primary forward-frontier targets
    wracają do ``secondary``. Demotion kaskaduje ich do ``ignored``
    (cascade — relacja p-tag była tylko-przez-tego-primary), a re-promote
    revivuje przez retro-skan ``_p_tag_pubkeys_for_author`` →
    ``_register_secondary_unlocked``. Tu sprawdzamy końcowy stan, fazy
    pośrednie pokrywa
    ``test_primary_demotion_cascades_dependents_to_ignored_and_repromote_revives``."""
    primary_prv, primary_pub = new_keypair()
    target_a_pub = new_pub()
    target_b_pub = new_pub()

    evt1 = signed_event(
        primary_prv, kind=1, content="hi a",
        tags=[["p", target_a_pub]], created_at=1,
    )
    evt2 = signed_event(
        primary_prv, kind=1, content="hi b",
        tags=[["p", target_b_pub]], created_at=2,
    )
    mock_relay.stage_event(evt1)
    mock_relay.stage_event(evt2)

    running_core.subscribe_to_pubkey(primary_pub, limit=10)
    DbHelper.expect_row(
        running_core, "SELECT 1 FROM events WHERE id = ?", [evt2["id"]],
    )

    # Oscylacja: demote kaskaduje targety do ignored, re-subscribe ma je
    # przywrócić przez retro-skan p-tagów wywołany przez track_pubkey.run.
    running_core.set_tracking_status(primary_pub, "secondary", limit=10)
    running_core.subscribe_to_pubkey(primary_pub, limit=10)

    DbHelper.expect_row(
        running_core,
        "SELECT 1 FROM tracked_pubkeys WHERE hex = ? AND role = 'secondary'",
        [target_a_pub],
    )
    DbHelper.expect_row(
        running_core,
        "SELECT 1 FROM tracked_pubkeys WHERE hex = ? AND role = 'secondary'",
        [target_b_pub],
    )
    assert _snapshot_roles(running_core) == {
        primary_pub: "primary",
        target_a_pub: "secondary",
        target_b_pub: "secondary",
    }


def test_ignored_promote_demote_keeps_events_intact(running_core, mock_relay):
    """Toggling primary → ignored → primary nie usuwa eventów — ``events``
    tabela nie jest sprzątana przez role transitions, a po promocji rola
    wraca do primary z `last_scan_at=NULL`."""
    prv, pub = new_keypair()
    evt = signed_event(prv, kind=1, content="hello", created_at=1)
    mock_relay.stage_event(evt)

    running_core.subscribe_to_pubkey(pub, limit=10)
    DbHelper.expect_row(
        running_core, "SELECT 1 FROM events WHERE id = ?", [evt["id"]],
    )

    running_core.set_tracking_status(pub, "ignored")
    running_core.subscribe_to_pubkey(pub, limit=10)

    assert _snapshot_roles(running_core) == {pub: "primary"}
    evt_rows = running_core.run_sql(
        "SELECT id FROM events WHERE id = ?", [evt["id"]],
    )
    assert evt_rows == [{"id": evt["id"]}]


def test_multi_pubkey_chaos_converges_to_expected_state(running_core, mock_relay):
    """Trzech primary p-taguje wspólnego targeta, potem A→ignored, B→secondary
    a C zostaje primary; shared target zarejestrowany przez forward frontier
    zostaje secondary, każde A/B/C ma rolę z ostatniego set_tracking_status."""
    a_prv, a_pub = new_keypair()
    b_prv, b_pub = new_keypair()
    c_prv, c_pub = new_keypair()
    shared_pub = new_pub()

    events = [
        signed_event(
            a_prv, kind=1, content="from a",
            tags=[["p", shared_pub]], created_at=1,
        ),
        signed_event(
            b_prv, kind=1, content="from b",
            tags=[["p", shared_pub]], created_at=2,
        ),
        signed_event(
            c_prv, kind=1, content="from c",
            tags=[["p", shared_pub]], created_at=3,
        ),
    ]
    for evt in events:
        mock_relay.stage_event(evt)

    running_core.subscribe_to_pubkey(a_pub, limit=10)
    running_core.subscribe_to_pubkey(b_pub, limit=10)
    running_core.subscribe_to_pubkey(c_pub, limit=10)

    # Sentinel: when the last event lands, all forward-frontier work is done.
    DbHelper.expect_row(
        running_core, "SELECT 1 FROM events WHERE id = ?", [events[-1]["id"]],
    )

    running_core.set_tracking_status(a_pub, "ignored")
    running_core.set_tracking_status(b_pub, "secondary", limit=10)
    # C left alone — still primary

    assert _snapshot_roles(running_core) == {
        a_pub: "ignored",
        b_pub: "secondary",
        c_pub: "primary",
        shared_pub: "secondary",
    }


def test_forward_frontier_respects_role_at_event_ingest_time(
    running_core, mock_relay,
):
    """Forward expansion działa tylko gdy AUTOR był primary w momencie
    ingestu — eventy zaingestowane PO demotion nie generują secondaries,
    nawet po rescan-ie (rescan ponownie ściąga eventy, ale role decision
    jest podejmowane na aktualnym stanie tabeli)."""
    prv, author_pub = new_keypair()
    target_early_pub = new_pub()
    target_late_pub = new_pub()

    evt_early = signed_event(
        prv, kind=1, content="early",
        tags=[["p", target_early_pub]], created_at=1,
    )
    mock_relay.stage_event(evt_early)

    # Subscribe as primary — evt_early ingests under role=primary, target_early
    # gets registered as secondary via forward frontier.
    running_core.subscribe_to_pubkey(author_pub, limit=10)
    DbHelper.expect_row(
        running_core, "SELECT 1 FROM events WHERE id = ?", [evt_early["id"]],
    )

    # Demote BEFORE the late event is staged or ingested.
    running_core.set_tracking_status(author_pub, "secondary", limit=10)

    evt_late = signed_event(
        prv, kind=1, content="late",
        tags=[["p", target_late_pub]], created_at=2,
    )
    mock_relay.stage_event(evt_late)
    # rescan forces auto-tracker to re-pull from the relay; evt_late lands now
    # but author is `secondary` so forward expansion is gated off.
    running_core.rescan_pubkey(author_pub)
    DbHelper.expect_row(
        running_core, "SELECT 1 FROM events WHERE id = ?", [evt_late["id"]],
    )

    assert _snapshot_roles(running_core) == {
        author_pub: "secondary",
        target_early_pub: "secondary",
        # target_late_pub deliberately absent — author was not primary
        # at the time evt_late ingested.
    }


def test_primary_demotion_cascades_dependents_to_ignored_and_repromote_revives(
    running_core, mock_relay,
):
    """Cascade demotion + revive symetrycznie.

    Demotion primary→secondary oznacza zależnych (secondaries trzymanych
    tylko-przez-tego primary) jako ``ignored``. Re-promote secondary→primary
    uruchamia retro-skan p-tagów (``_p_tag_pubkeys_for_author`` →
    ``_register_secondary_unlocked``), który revivuje ich z powrotem do
    ``secondary``."""
    a_prv, a_pub = new_keypair()
    dep_pub = new_pub()

    # Do bazy trafia primary i jego tagi.
    evt = signed_event(
        a_prv, kind=1, content="hi dep",
        tags=[["p", dep_pub]], created_at=1,
    )
    mock_relay.stage_event(evt)
    running_core.subscribe_to_pubkey(a_pub, limit=10)
    DbHelper.expect_row(
        running_core, "SELECT 1 FROM events WHERE id = ?", [evt["id"]],
    )

    # Dalej: do bazy trafia secondary (forward frontier z eventu primary).
    DbHelper.expect_row(
        running_core,
        "SELECT 1 FROM tracked_pubkeys WHERE hex = ? AND role = 'secondary'",
        [dep_pub],
    )

    # DALEJ: degradujemy z primary do secondary.
    running_core.set_tracking_status(a_pub, "secondary", limit=10)

    # Sprawdzamy czy udała się degradacja z primary do secondary.
    row_a = DbHelper.expect_row(
        running_core,
        "SELECT role FROM tracked_pubkeys WHERE hex = ?",
        [a_pub],
    )
    assert row_a == {"role": "secondary"}

    # Czy udała się degradacja zależnego z secondary do ignored.
    row_dep = DbHelper.expect_row(
        running_core,
        "SELECT role FROM tracked_pubkeys WHERE hex = ?",
        [dep_pub],
    )
    assert row_dep == {"role": "ignored"}, (
        "demotion primary→secondary powinna kaskadować zależnych do ignored"
    )

    # DALEJ: awansujemy z powrotem z secondary do primary.
    running_core.subscribe_to_pubkey(a_pub, limit=10)

    # Sprawdzamy czy zależny awansował automatycznie z ignored do secondary,
    # bo mamy zapytanie do relay (track_pubkey.run retro-skanuje p-tagi
    # eventów świeżo promowanego primary i revivuje ignored).
    revived = DbHelper.expect_row(
        running_core,
        "SELECT role, last_scan_at FROM tracked_pubkeys "
        "WHERE hex = ? AND role = 'secondary'",
        [dep_pub],
    )
    assert revived["role"] == "secondary"
    assert revived["last_scan_at"] is None
