"""Contract: the SDK's monitor path against a REAL broker (M-B B3).

`test_wire_subscribe.py` pins the broker's subscribe/drain wire shapes
and stays UNMODIFIED — it is the compat oracle for this chunk. This file
is the other half: what the SDK does with those shapes now that
`monitor` / `amonitor` / `monitor_fd` consume a server-side subscription
instead of polling.

Five claims, all against a live broker rather than a fake, because every
one of them is about a fact the fake would have to assert on the SDK's
behalf:

  * **Latency.** Another operator's claim reaches the consumer's fd in
    milliseconds. The old pump slept `poll_interval_s` between drains
    and could not beat half an interval on average; the budget here is
    500 ms, which is ~500 times the measured number and generous enough to
    survive a contended CI agent.
  * **`wait_ms` really rides the wire, and an empty long poll is a
    successful answer** — not an error, not a timeout, not a poisoned
    transport.
  * **Poison avoidance (B3-AM1).** A poll that completes AT `wait_ms`
    leaves the connection healthy and usable; a deadline sized inside
    the wait window is refused before any wire traffic. The second
    claim is the one that matters: the failure it prevents is not "a
    short poll" but "every later call on this connection refused".
  * **Crash-only reconnect (D-024).** Kill the broker under an active
    monitor, restart it: the consumer gets a `monitor_reconnect` gap,
    the stream re-subscribes on a new connection, and events flow
    again.
  * **The default filter is `scope`, not `urn_glob(scope)`** — a wire
    pin for the one place this SDK deliberately departs from "predicate
    = urn_glob(scope)". A gap event carries no URN, so a `urn_glob`
    PREDICATE drops every one of them; the broker's own suite pins both
    halves (`subscription_test.zig`: "Gap has no URN; urn_glob should
    reject" and "gap events pass to every subscription regardless of
    scope"). Losing gaps is losing D-022.

The event source is a claim/release on `hw:/test/dev0` from a SECOND
connection — the multi-operator shape the milestone exists for, and the
same one `test_wire_core_events.py` uses.
"""

from __future__ import annotations

import json
import os
import select
import socket
import threading
import time
from typing import TYPE_CHECKING, Any

import pytest

from mgf.hrb import Client, MgfHrbError, kind_in, urn_glob
from tests import _broker

if TYPE_CHECKING:
    import subprocess
    from collections.abc import Iterator

pytestmark = pytest.mark.contract

# The synthetic worker every broker carries for wire tests; claiming it
# touches no real hardware (`server.zig` seeds it).
_URN = "hw:/test/dev0"

# Event → fd readable. Measured at 0.13-0.87 ms across five claims on
# this bench; the budget is three orders of magnitude of headroom
# because a CI agent under load is not this bench, and a latency pin
# that flakes teaches people to ignore latency pins.
_LATENCY_BUDGET_S = 0.5


class _OwnBroker:
    """A broker this file owns, and can kill and replace mid-test.

    The shared `broker` / `raw_broker` fixtures cannot survive the
    reconnect pin: they hold the process handle privately and tear it
    down at the end, so a test that kills the broker leaves the fixture
    reaping a corpse and the next test inheriting a dead socket. This
    type keeps the handle where the test can reach it — and, more
    importantly, keeps ONE authority on which process is currently
    listening, so teardown is correct however many restarts happened."""

    def __init__(self, path: str) -> None:
        self.path = path
        self._proc: subprocess.Popen[bytes] | None = _broker.spawn_broker(path)

    def kill(self) -> None:
        """SIGTERM the current broker and wait for the socket to go."""
        if self._proc is not None:
            _broker.teardown_broker(self._proc, self.path)
            self._proc = None

    def restart(self) -> None:
        """Bring a REPLACEMENT up on the same path. Deliberately a new
        process: D-024 is about crash-only restart, and a broker that
        restarts keeps none of its soft state — the subscriptions the
        SDK registered on the old one are gone, which is precisely what
        the reconnect exists to notice."""
        self.kill()
        self._proc = _broker.spawn_broker(self.path)


@pytest.fixture
def own_broker() -> Iterator[_OwnBroker]:
    """A broker with its own socket path, spawned and torn down here."""
    if not _broker.server_exists():
        pytest.skip(
            f"Broker binary not built at {_broker.server_path()}. Run: {_broker.build_command()}"
        )
    is_stale, reason = _broker.broker_is_stale()
    if is_stale:
        pytest.fail(reason, pytrace=False)
    own = _OwnBroker(_broker.default_sock_path().replace(".sock", "-b3.sock"))
    try:
        yield own
    finally:
        own.kill()


def _read_events(fd: int, *, budget_s: float) -> list[dict[str, Any]]:
    """Every complete JSON line readable within the budget."""
    out: list[dict[str, Any]] = []
    buf = b""
    deadline = time.monotonic() + budget_s
    while time.monotonic() < deadline:
        readable, _, _ = select.select([fd], [], [], max(0.0, deadline - time.monotonic()))
        if not readable:
            break
        chunk = os.read(fd, 65536)
        if not chunk:
            break
        buf += chunk
        while b"\n" in buf:
            line, _, buf = buf.partition(b"\n")
            if line:
                out.append(json.loads(line))
        if out:
            break
    return out


# ---------------------------------------------------------------------------
# Latency — the headline of MB-3.
# ---------------------------------------------------------------------------


@pytest.mark.timeout(60)
def test_another_operators_claim_reaches_the_fd_in_milliseconds(own_broker: _OwnBroker) -> None:
    """The chunk's reason for existing: an event that happens NOW is
    readable NOW, not on the next tick of a client-side timer."""
    watcher = Client.connect(own_broker.path)
    actor = Client.connect(own_broker.path, client_id=7)
    latencies: list[float] = []
    try:
        fd = watcher.monitor_fd("hw:/test/**", wait_ms=5_000)
        # Let the subscription settle before the first mutation: a claim
        # that lands before the subscribe is not a latency measurement,
        # it is a race.
        time.sleep(0.1)

        for _ in range(3):
            started = time.monotonic()
            lease = actor.claim(_URN, "exclusive")
            events = _read_events(fd, budget_s=_LATENCY_BUDGET_S)
            latencies.append(time.monotonic() - started)
            assert events, "no event reached the fd within the budget"
            assert events[0]["kind"] == "ownership_changed", events
            assert events[0]["urn"] == _URN
            lease.release()
            _read_events(fd, budget_s=_LATENCY_BUDGET_S)
    finally:
        actor.close()
        watcher.close()

    worst = max(latencies)
    assert worst < _LATENCY_BUDGET_S, (
        f"claim → fd readable took {worst * 1000:.1f}ms, budget "
        f"{_LATENCY_BUDGET_S * 1000:.0f}ms (all: "
        f"{[round(x * 1000, 2) for x in latencies]})"
    )


# ---------------------------------------------------------------------------
# `wait_ms` on the wire, and the empty answer.
# ---------------------------------------------------------------------------


def test_a_long_poll_that_finds_nothing_answers_empty_and_stays_healthy(
    own_broker: _OwnBroker,
) -> None:
    """The whole long-poll contract in one test: the broker holds the
    request for `wait_ms`, answers with an empty list rather than an
    error, and the connection is fine afterwards.

    That last clause is the one with teeth. The call blocks for the full
    wait, so it is exactly the shape that would trip a mis-sized
    client-side deadline — and a tripped deadline does not return late,
    it POISONS the transport (F-A1-01). Proving the connection still
    works is proving the SDK sized its own deadline correctly."""
    client = Client.connect(own_broker.path)
    try:
        sub = client.subscribe("longpoll-probe", "hw:/test/**")
        started = time.monotonic()
        events = client.drain_events(subscription_id=sub.id, wait_ms=300, deadline_ms=300 + 2_000)
        elapsed = time.monotonic() - started
        assert events == [], f"expected an empty answer, got {events}"
        assert elapsed >= 0.25, (
            f"the drain returned after {elapsed * 1000:.0f}ms — the broker did not "
            "hold the request, so wait_ms never reached it"
        )
        # NOT poisoned: the connection answers the very next call.
        assert client.predicate_grammar_version() == "1.0.0"
    finally:
        client.close()


def test_the_monitor_asks_the_broker_to_wait(own_broker: _OwnBroker) -> None:
    """The SDK's own monitor path must send `wait_ms` and a
    `subscription_id` — the two things that make it a subscription
    consumer rather than the pre-B3 poller. Read off the broker's audit
    ring, which is the broker's account of what arrived, not the SDK's
    account of what it sent."""
    client = Client.connect(own_broker.path)
    try:
        client.monitor_fd("hw:/test/**", wait_ms=200)
        # Longer than one wait: a drain is AUDITED when it completes, and
        # on a quiet bench it completes at wait_ms. Sleeping less than
        # that reads an empty ring and proves nothing — which is itself
        # evidence the wait reached the broker.
        time.sleep(0.6)
        records = client.audit_tail(50)
        ops = [r.op for r in records.records]
        assert "subscribe" in ops, ops
        assert "drain_events" in ops, ops
    finally:
        client.close()


# ---------------------------------------------------------------------------
# B3-AM1 — the refusals, and what they are protecting.
# ---------------------------------------------------------------------------


def test_a_mis_sized_deadline_is_refused_before_any_wire_traffic(own_broker: _OwnBroker) -> None:
    """The invariant is enforced client-side because it CANNOT be
    enforced anywhere else: the broker's `DrainEventsReq` carries no
    `deadline_ms` field at all, so nothing server-side can notice that a
    5 s wait was armed with a 1 s bound.

    Both refusals fire before a byte is sent, which is checked here
    against the broker's own audit ring — a refusal that has already
    spoken to the broker has already had the chance to poison it."""
    client = Client.connect(own_broker.path)
    try:
        before = len(client.audit_tail(200).records)

        with pytest.raises(ValueError, match="B3-AM1"):
            client.drain_events(wait_ms=5_000, deadline_ms=1_000)
        with pytest.raises(ValueError, match="B3-AM1"):
            client.monitor_fd("hw:/test/**", wait_ms=5_000, deadline_ms=1_000)
        with pytest.raises(ValueError, match="unbounded"):
            client.monitor_fd("hw:/test/**", wait_ms=5_000, deadline_ms=0)
        with pytest.raises(ValueError, match="ceiling"):
            client.monitor_fd("hw:/test/**", wait_ms=30_001)

        after = client.audit_tail(200).records
        # Only the two audit_tail calls themselves are new.
        new_ops = [r.op for r in after[: len(after) - before]]
        assert all(op == "audit_tail" for op in new_ops), (
            f"a refused call reached the broker anyway: {new_ops}"
        )
    finally:
        client.close()


def test_the_brokers_own_ceiling_refuses_rather_than_clamps(own_broker: _OwnBroker) -> None:
    """The SDK mirrors a refusal it did not invent. Proven by going
    around the SDK's own check — raw wire — so this pin fails if the
    BROKER ever starts clamping instead."""
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(own_broker.path)
    try:
        buf = b""
        while b"\n" not in buf:
            buf += s.recv(4096)
        s.sendall(
            json.dumps({"op": "drain_events", "scope": "hw:/**", "wait_ms": 30_001}).encode()
            + b"\n"
        )
        buf = b""
        while b"\n" not in buf:
            buf += s.recv(4096)
        resp = json.loads(buf.split(b"\n", 1)[0])
        assert resp.get("ok") is False, resp
        assert resp.get("err") == "BadRequest", resp
    finally:
        s.close()


# ---------------------------------------------------------------------------
# Crash-only reconnect (D-024).
# ---------------------------------------------------------------------------


@pytest.mark.timeout(90)
def test_a_broker_restart_under_a_live_monitor_gaps_then_recovers(own_broker: _OwnBroker) -> None:
    """MB-4, at the SDK level. The broker dies under an active
    `monitor_fd` and comes back; the consumer is TOLD (a gap, which is
    already every consumer's "rescan" signal) and then keeps receiving
    events on a re-subscribed stream.

    No claim is made about the events that fell in the hole — the
    broker's ring died with it, and a gap means "resynchronise from the
    source", never "here is what you missed"."""
    watcher = Client.connect(own_broker.path)
    try:
        fd = watcher.monitor_fd("hw:/test/**", wait_ms=5_000)
        time.sleep(0.1)

        # Kill it. The pump's blocked drain sees EOF within a
        # millisecond; the replacement is not listening yet, which is
        # exactly why the reconnect is a WINDOW and not one syscall.
        own_broker.restart()

        gap_lines = _read_events(fd, budget_s=10.0)
        assert gap_lines, "the consumer was told nothing about the restart"
        assert gap_lines[0]["kind"] == "gap"
        assert gap_lines[0]["source"] == "monitor_reconnect", gap_lines[0]
        assert gap_lines[0]["error"], "the gap must carry the cause"

        # …and the stream is alive on the NEW broker.
        actor = Client.connect(own_broker.path, client_id=9)
        try:
            time.sleep(0.3)
            lease = actor.claim(_URN, "exclusive")
            events = _read_events(fd, budget_s=5.0)
            assert events, (
                "no events after the reconnect — the stream reported a gap and "
                "then died quietly, which is the one outcome worse than dying loudly"
            )
            assert events[0]["kind"] == "ownership_changed", events
            lease.release()
        finally:
            actor.close()
    finally:
        watcher.close()


# ---------------------------------------------------------------------------
# The predicate default — where this SDK departs from `urn_glob(scope)`.
# ---------------------------------------------------------------------------


def test_the_default_monitor_sends_scope_and_no_predicate(own_broker: _OwnBroker) -> None:
    """With no `predicate`, the subscribe carries `scope` and NO
    predicate field.

    This is the chunk's one deliberate departure from its own packet,
    and it is not a preference. `scope` is applied by the broker as a
    GAP-HONEST pre-filter; a `urn_glob` PREDICATE is applied by the
    evaluator, which reads the event's URN — and a gap has none, so it
    evaluates false and the subscription silently stops hearing "you
    missed things". The broker's own suite pins both halves
    (`subscription_test.zig`: "Gap has no URN; urn_glob should reject";
    "registry: gap events pass to every subscription regardless of
    scope"), and `openHiddenLocked` makes the same choice for the same
    reason in the broker's own legacy path.

    Instrumented at `Client.subscribe` rather than at a `Client` this
    test holds, because the subscribe that matters is issued by the
    MONITOR's own connection, which the SDK opens internally. An earlier
    version of this pin called `client.subscribe(...)` directly and
    therefore proved nothing about the monitor at all: seeding
    `predicate = urn_glob(scope)` into the monitor's own path left it
    green (sabotage round S5). The instrument goes where the defect
    would be.
    """
    seen: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
    original = Client.subscribe

    def recording(self: Client, *args: Any, **kwargs: Any) -> Any:
        seen.append((args, dict(kwargs)))
        return original(self, *args, **kwargs)

    client = Client.connect(own_broker.path)
    try:
        Client.subscribe = recording  # type: ignore[method-assign, assignment]
        try:
            client.monitor_fd("hw:/test/**", wait_ms=50)
        finally:
            Client.subscribe = original  # type: ignore[method-assign]
    finally:
        client.close()

    assert len(seen) == 1, f"expected exactly one subscribe from the monitor, got {seen}"
    args, kwargs = seen[0]
    assert args[1] == "hw:/test/**", f"the subtree must ride as scope: {args}"
    assert kwargs.get("predicate") is None, (
        "the default monitor must NOT send urn_glob(scope) as a predicate — "
        "it would drop every gap event, and 'you missed things' is the one "
        f"message a monitor may never lose (D-022). Got {kwargs.get('predicate')!r}"
    )


def test_a_caller_supplied_predicate_reaches_the_broker_and_filters(
    own_broker: _OwnBroker,
) -> None:
    """The other side of the same coin: a predicate the CALLER asked for
    is honoured server-side. `kind_in("added")` cannot match an
    `ownership_changed`, so a claim produces nothing on this
    subscription — while the same claim on an unfiltered one does."""
    watcher = Client.connect(own_broker.path)
    actor = Client.connect(own_broker.path, client_id=11)
    try:
        filtered = watcher.subscribe("filtered", "hw:/test/**", predicate=kind_in("added"))
        unfiltered = watcher.subscribe("unfiltered", "hw:/test/**")

        lease = actor.claim(_URN, "exclusive")
        try:
            assert watcher.drain_events(subscription_id=unfiltered.id, wait_ms=1_000), (
                "the unfiltered subscription saw nothing — the event never happened"
            )
            assert watcher.drain_events(subscription_id=filtered.id, wait_ms=200) == [], (
                "kind_in('added') matched an ownership_changed event"
            )
        finally:
            lease.release()
    finally:
        actor.close()
        watcher.close()


def test_the_builders_emit_what_the_broker_accepts(own_broker: _OwnBroker) -> None:
    """Grammar-v1 shape equality, verified by the only authority that
    counts: the broker parses every builder's output without complaint,
    and refuses the empty forms the builders refuse client-side."""
    client = Client.connect(own_broker.path)
    try:
        assert client.predicate_grammar_version() == "1.0.0"
        for i, predicate in enumerate(
            [
                kind_in("added", "removed", "ownership_changed"),
                urn_glob("hw:/test/**"),
                {"attr_eq": {"key": "kind", "value": "added"}},
                {"attr_in": {"key": "kind", "values": ["added", "removed"]}},
                {"all_of": [kind_in("added"), urn_glob("hw:/test/**")]},
                {"any_of": [kind_in("added"), kind_in("removed")]},
                {"not": kind_in("gap")},
            ]
        ):
            client.subscribe(f"grammar-{i}", "hw:/test/**", predicate=predicate)
    finally:
        client.close()


def _kill_after(own_broker: _OwnBroker, delay_s: float, killed_at: list[float]) -> None:
    """Kill the broker `delay_s` from now, on a side thread.

    The kill has to land while a drain is ALREADY parked in its long
    poll — that is the state the ordering pin is about. For `monitor`
    and `amonitor` the drain is reached only from inside the blocking
    `next()` / `anext()`, so the test thread cannot do the killing
    itself."""

    def _run() -> None:
        time.sleep(delay_s)
        killed_at.append(time.monotonic())
        own_broker.kill()

    threading.Thread(target=_run, daemon=True).start()


def _first_event_after_death(
    own_broker: _OwnBroker, client: Client, shape: str
) -> tuple[dict[str, Any], float]:
    """Start `shape`'s monitor, kill the broker under it, and return the
    first thing the consumer receives plus how long after the kill it
    arrived."""
    killed_at: list[float] = []
    settle_s = 0.3

    if shape == "monitor_fd":
        fd = client.monitor_fd("hw:/test/**", wait_ms=5_000)
        _kill_after(own_broker, settle_s, killed_at)
        events = _read_events(fd, budget_s=10.0)
        first = events[0] if events else {}
    elif shape == "monitor":
        it = iter(client.monitor("hw:/test/**", wait_ms=5_000))
        _kill_after(own_broker, settle_s, killed_at)
        # The connect + subscribe happen here, in the first millisecond;
        # the kill is 300 ms away, so the failure this measures is
        # always a DRAIN failure and never a setup one.
        first = next(it)
    else:
        import asyncio

        async def _run() -> dict[str, Any]:
            agen = client.amonitor("hw:/test/**", wait_ms=5_000)
            _kill_after(own_broker, settle_s, killed_at)
            return await anext(agen)  # type: ignore[arg-type]

        first = asyncio.run(_run())

    assert killed_at, "the killer thread never ran"
    return first, time.monotonic() - killed_at[0]


@pytest.mark.timeout(90)
@pytest.mark.parametrize("shape", ["monitor", "amonitor", "monitor_fd"])
def test_the_gap_is_emitted_before_the_reconnect_is_attempted(
    own_broker: _OwnBroker, shape: str
) -> None:
    """Ordering pin (F-B3-02), over ALL THREE shapes (RC-B3-01).

    The gap goes out first, and against a broker that never comes back
    it is the only thing that goes out before the terminal one.

    Invert the order — announce the discontinuity only after a
    successful reconnect — and a permanently dead broker produces no
    `monitor_reconnect` gap at all: the reconnect never succeeds, so the
    announcement never happens. That is D-022 lost in the one case that
    matters most, and it looks identical to the correct code whenever
    the broker DOES come back, which is the only case a happy-path test
    exercises.

    **Why all three, and not just the fd.** The first version of this
    pin covered `monitor_fd` alone, because that is where the ordering
    is written most visibly — an explicit `_write_line` before the
    `stream.reconnect()`. The re-court then inverted the SAME two lines
    in `_amonitor_iter` and in `_monitor_iter` independently and got 786
    and 693 green tests. Three shapes, one contract, three pins: a
    generator's `yield` before a call is an ordering as real as a
    write's, and just as invisible to a suite that only watches the
    pipe.

    The measurement that makes the ordering visible is TIME: the
    reconnect window is ~2 s, so a gap that arrives inside a second
    arrived before the attempt finished, not after it."""
    client = Client.connect(own_broker.path)
    try:
        first, delay = _first_event_after_death(own_broker, client, shape)
    finally:
        client.close()

    assert first, f"{shape}: a dead broker produced no gap at all"
    assert first["kind"] == "gap", first
    assert first["source"] == "monitor_reconnect", first
    assert first["error"], f"{shape}: the gap must carry the cause"
    assert delay < 1.0, (
        f"{shape}: the reconnect gap took {delay:.2f}s to arrive — longer "
        "than a poll's worth of dialling, which means it was emitted AFTER "
        "the reconnect attempt rather than before it"
    )


@pytest.mark.timeout(60)
def test_the_fd_shape_still_dies_honestly_after_its_gap(own_broker: _OwnBroker) -> None:
    """The other half of the fd shape's contract: once the reconnect
    window lapses against a broker that never returns, the terminal
    `monitor_pump_error` gap and EOF follow the reconnect gap. Split out
    of the ordering pin above so the ordering claim is measured on all
    three shapes without asserting a pipe-only fact three times."""
    watcher = Client.connect(own_broker.path)
    try:
        fd = watcher.monitor_fd("hw:/test/**", wait_ms=5_000)
        time.sleep(0.1)
        own_broker.kill()

        first = _read_events(fd, budget_s=10.0)
        assert first and first[0]["source"] == "monitor_reconnect", first
        terminal = _read_events(fd, budget_s=10.0)
        assert terminal, "the stream went quiet instead of ending"
        assert terminal[0]["source"] == "monitor_pump_error", terminal[0]
    finally:
        watcher.close()


def test_the_streams_setup_call_carries_its_own_short_deadline(own_broker: _OwnBroker) -> None:
    """F-B3-04. The `subscribe` that opens a stream is bounded by
    `_OPEN_DEADLINE_MS` (2 s), NOT by the drain's `wait_ms + 2000`.

    A setup call has nothing to do with how long a poll may wait, and
    conflating them has a user-visible cost: `monitor_fd` opens on the
    CALLER's thread, so a consumer who asked for a 30 s poll would wait
    32 s to be told the broker is wedged. Pinned by reading the deadline
    off the wire request rather than off the constant, so a change to
    either is visible here."""
    seen: list[dict[str, Any]] = []
    client = Client.connect(own_broker.path)
    try:
        original = client._t.call

        def recording(**req: Any) -> dict[str, Any]:
            seen.append(dict(req))
            return original(**req)

        # The monitor opens its OWN connection, so patch at the class
        # level and let the stream's client inherit it.
        original_connect = Client.connect

        def patched(path: str, **kwargs: Any) -> Client:
            c = original_connect(path, **kwargs)
            inner = c._t.call

            def rec(**req: Any) -> dict[str, Any]:
                seen.append(dict(req))
                return inner(**req)

            c._t.call = rec  # type: ignore[method-assign]
            return c

        Client.connect = staticmethod(patched)  # type: ignore[method-assign, assignment]
        try:
            client.monitor_fd("hw:/test/**", wait_ms=10_000)
            time.sleep(0.2)
        finally:
            Client.connect = original_connect  # type: ignore[method-assign]
            client._t.call = original  # type: ignore[method-assign]
    finally:
        client.close()

    subscribes = [r for r in seen if r.get("op") == "subscribe"]
    drains = [r for r in seen if r.get("op") == "drain_events"]
    assert subscribes, seen
    assert subscribes[0]["deadline_ms"] == 2_000, (
        f"the setup subscribe carried deadline_ms={subscribes[0].get('deadline_ms')}; "
        "it must carry the stream's own 2s setup bound, not the drain's "
        "wait_ms + 2000 — otherwise a 30s poll means a 32s wait to learn "
        "the broker is wedged"
    )
    assert drains, "the pump never drained"
    assert drains[0]["deadline_ms"] == 10_000 + 2_000, drains[0]


@pytest.mark.timeout(60)
def test_a_wedged_broker_fails_monitor_fd_at_the_setup_bound_not_the_poll_bound() -> None:
    """The same fact, end to end and on the clock (F-B3-04, sabotage
    S15). A broker that completes the hello and then answers nothing
    must fail `monitor_fd(wait_ms=30_000)` in about two seconds — the
    setup bound — and not in thirty-two."""
    import tempfile
    from pathlib import Path as _Path

    tmp = tempfile.mkdtemp(prefix="b3-wedge-")
    _ = _Path(tmp)
    # `shed_first` is not wedging; a wedge needs a peer that answers the
    # hello and then nothing at all, which the deadline suite's fake
    # already models. Reuse it rather than growing a third fake.
    from tests.unit.mgf.hrb.test_sdk10_deadlines import _FakeBroker

    wedge = _FakeBroker("wedge")
    try:
        client = Client.connect(wedge.path)
        try:
            started = time.monotonic()
            with pytest.raises(MgfHrbError):
                client.monitor_fd("hw:/test/**", wait_ms=30_000)
            elapsed = time.monotonic() - started
        finally:
            client.close()
    finally:
        wedge.stop()

    assert elapsed < 6.0, (
        f"monitor_fd(wait_ms=30000) against a wedged broker took {elapsed:.1f}s; "
        "the setup bound is 2s and must not scale with the poll budget"
    )
