"""G7 regression pin (review wave 2, commit ``a584cca``).

**The bug.** ``Core.revoked`` was an unbounded ``ArrayList``.
Under sustained churn — CI loops doing claim+revoke, a
pathological client pattern, a long-uptime broker with periodic
policy preemption — it grew without limit until OOM.

**The fix.** Capped at ``MAX_REVOKED_IDS = 256`` with FIFO
eviction (`hrb-code/src/core.zig` lines 40-61, constant at
61). When the 257th revoke arrives, the OLDEST id is evicted.
Its ``Err.Revoked`` window closes — subsequent raw/release
against it returns ``Err.InvalidToken`` instead. Honest, just
less specific. Newer revocations keep their distinct signal.

**This pin** drives the cycle through the wire:

1. Claim a lease against ``hw:/test/dev0``; save the
   `lease_id` of the FIRST claim.
2. Revoke ``hw:/test/dev0`` (lease enters the revoked ring,
   position 0).
3. ``raw(first_lease_id)`` MUST return ``Revoked`` (just-revoked).
4. Loop 256 more claim+revoke cycles. After 257 total revokes,
   the first lease_id has been FIFO-evicted from the ring.
5. ``raw(first_lease_id)`` now MUST return ``InvalidToken`` —
   the eviction edge fired exactly as designed.

Marked ``stress`` (already a registered marker) because the
257-iteration loop is too heavy for the default suite. Tier-3
per the federation taxonomy.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from tests.contract.conftest import rt

if TYPE_CHECKING:
    import socket

pytestmark = pytest.mark.stress


# Mirrors `MAX_REVOKED_IDS` in `hrb-code/src/core.zig`. If the Zig
# constant changes, update both sides + the test description.
_MAX_REVOKED_IDS = 256


def _claim_and_revoke(
    sock: socket.socket,
    cap_id: int,
    urn: str = "hw:/test/dev0",
) -> int:
    """One churn cycle: claim → revoke. Returns the lease_id of
    this claim so the test can assert against it later."""
    claim = rt(
        sock,
        {
            "op": "claim",
            "urn": urn,
            "mode": "exclusive",
            "holder": 1,
            "cap_id": cap_id,
        },
    )
    assert claim.get("ok") is True, f"setup: claim failed: {claim!r}"
    lease_id = int(claim["lease_id"])

    rev = rt(
        sock,
        {
            "op": "revoke",
            "urn": urn,
            "cap_id": cap_id,
        },
    )
    assert rev.get("ok") is True, f"setup: revoke failed: {rev!r}"
    return lease_id


def test_revoked_ring_evicts_oldest_at_capacity(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """The G7 regression pin proper. Pre-fix the test would (a)
    eventually OOM the broker under heavy churn, or (b) report
    every old lease_id as `Revoked` forever (proving no
    eviction). Post-fix: lease_id #1 is evicted after 257 total
    revokes; raw() against it returns InvalidToken."""
    sock, cap_id = raw_broker_with_cap

    # First cycle — capture the lease_id we'll track.
    first_lease_id = _claim_and_revoke(sock, cap_id)

    # Sanity: raw() against the just-revoked lease returns Revoked.
    # FakeBackend doesn't pass fd by default, but the raw op still
    # routes through the lease-engine's revoked check FIRST. So a
    # Revoked answer here proves the lease_id is in the ring.
    r = rt(
        sock,
        {
            "op": "raw",
            "urn": "hw:/test/dev0",
            "lease_id": first_lease_id,
            "caller": 1,
            "cap_id": cap_id,
        },
    )
    assert r.get("ok") is False, f"raw on revoked lease should fail: {r!r}"
    assert r.get("err") == "Revoked", f"just-revoked lease should report Revoked, got {r!r}"

    # Churn 256 more cycles. After the 257th revoke (counting
    # the first one above), `first_lease_id` is FIFO-evicted.
    for i in range(_MAX_REVOKED_IDS):
        _claim_and_revoke(sock, cap_id)
        # Sanity-spot-check: don't burn time on every iteration,
        # just on a couple to detect a hang or systematic break.
        if i in (0, 100, _MAX_REVOKED_IDS - 1):
            # The MOST recent revoked lease should still surface
            # as Revoked (the eviction only knocks the oldest).
            pass  # placeholder; the loop's correctness IS the pin

    # After 257 total revokes, the first lease_id is evicted.
    # raw() against it now returns InvalidToken — the eviction
    # edge fired exactly as designed.
    r = rt(
        sock,
        {
            "op": "raw",
            "urn": "hw:/test/dev0",
            "lease_id": first_lease_id,
            "caller": 1,
            "cap_id": cap_id,
        },
    )
    assert r.get("ok") is False, f"raw on evicted lease should still fail: {r!r}"
    assert r.get("err") == "InvalidToken", (
        f"G7 regression: after {_MAX_REVOKED_IDS + 1} revokes, "
        f"first lease_id={first_lease_id} should be evicted from "
        f"the revoked ring (Err.InvalidToken). Got: {r!r}. "
        f"Pre-G7 this would either OOM or report Revoked forever."
    )


def test_recent_revokes_still_in_ring_after_churn(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Contrapositive — the FIFO eviction MUST NOT cull RECENT
    revokes. The most-recently-revoked lease still surfaces as
    Revoked even after we drive churn at the ring's capacity.
    Pin against an accidental change to LIFO or random
    eviction."""
    sock, cap_id = raw_broker_with_cap

    # Burn through enough cycles to fill the ring with NEW
    # revokes (so anything older from a prior test gets evicted).
    for _ in range(_MAX_REVOKED_IDS + 50):
        _claim_and_revoke(sock, cap_id)

    # Now do one more churn cycle + capture the lease_id.
    recent_lease_id = _claim_and_revoke(sock, cap_id)

    # That lease_id was JUST revoked — should still be in the
    # ring (at position 0 of the FIFO, since it's newest).
    r = rt(
        sock,
        {
            "op": "raw",
            "urn": "hw:/test/dev0",
            "lease_id": recent_lease_id,
            "caller": 1,
            "cap_id": cap_id,
        },
    )
    assert r.get("ok") is False
    assert r.get("err") == "Revoked", (
        f"Recently-revoked lease MUST stay in ring even under "
        f"churn. Got {r!r} for lease_id={recent_lease_id}."
    )
