"""End-to-end: REAL Python client (mgf.hrb) ↔ REAL Zig broker, over a
Unix socket. Proves the no-FFI boundary (Part 8.1 / D-005): Python links
zero Zig, shares no memory, speaks only bytes on a socket. Two processes,
two languages, one protocol.

Run from repo root after building the broker::

    cd hrb-code && zig build
    python tests/e2e/test_e2e.py

When this file is invoked via `pytest`, the function below also serves
as a standard test (pytest collects functions named `test_*`); when
invoked directly (`python tests/e2e/test_e2e.py`), the `__main__` block
runs the same checks with an exit code.
"""

from __future__ import annotations

import json
import os
import signal
import socket
import subprocess
import sys
import time
from dataclasses import dataclass, field
from pathlib import Path

import pytest
from tests import _broker

# Module-level marker — every test in this file is e2e by default.
# `addopts = "-m 'not e2e'"` in pyproject.toml excludes from the
# default suite; run via `pytest -m e2e` after `cd hrb-code &&
# zig build` so the broker binary exists + is launchable.
pytestmark = [pytest.mark.e2e, pytest.mark.timeout(120)]

# Allow running without an editable install — find src/ relative to this file.
_REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(_REPO_ROOT / "src"))

from mgf.hrb import (  # noqa: E402
    BackendCapability,
    ClaimMode,
    Client,
    ConflictError,
    ControlVerb,
    DoctorFinding,
    DoctorSeverity,
    Family,
    FieldFidelity,
    Kind,
    LeaseRecord,
    MgfHrbError,
    NotPermittedError,
    ResourceDetail,
    UnsupportedError,
)

SOCK = "/tmp/mgf-hrb.sock"  # fixed broker socket path per design D-005
SERVER = _REPO_ROOT / "hrb-code" / "zig-out" / "bin" / "mgf-hrb-server"

# Cap on how much broker output rides in the assertion message (chars).
# The e2e broker prints ~2 lines; a panicking one could spill a KB-scale
# backtrace — bound it so the junit `<failure>` stays readable, keeping
# the tail (where the panic reason lands).
_BROKER_OUTPUT_TAIL = 4000


@dataclass
class E2EResult:
    """Outcome of one :func:`run_e2e` invocation.

    ``exit_code`` preserves the historical contract (``0`` all checks
    passed · ``1`` at least one check failed · ``2`` broker binary not
    built). The remaining fields are diagnostic fuel: :meth:`report`
    weaves them into the pytest assertion message so a red e2e explains
    itself in the mtest failure bundle.

    Why the assertion message specifically: the bundle is built from the
    junit ``<failure>`` element, and pytest's default ``junit_logging =
    "no"`` keeps captured stdout OUT of the junit — so ``run_e2e``'s
    printed PASS/FAIL lines never reach it. Before this fix the bundle's
    stdout/stderr came back empty and a red e2e was undiagnosable; the
    assertion message is the one channel that survives into the bundle.
    """

    exit_code: int
    failed_checks: list[str] = field(default_factory=list)
    broker_exit: int | None = None
    broker_output: str = ""

    def report(self) -> str:
        """Human-readable failure report for the assertion message —
        names the failed checks, the broker's exit code, and its
        captured stdout/stderr (tailed to keep the message bounded)."""
        lines = [f"E2E run failed (exit {self.exit_code})"]
        if self.failed_checks:
            lines.append(f"{len(self.failed_checks)} check(s) failed:")
            lines += [f"  - {name}" for name in self.failed_checks]
        lines.append(f"broker exit code: {self.broker_exit}")
        lines.append("broker stdout/stderr (merged):")
        out = self.broker_output.strip()
        if not out:
            lines.append("  (broker emitted nothing on stdout/stderr)")
        else:
            tail = out[-_BROKER_OUTPUT_TAIL:]
            if len(out) > _BROKER_OUTPUT_TAIL:
                tail = "…(truncated)…\n" + tail
            lines += [f"  | {ln}" for ln in tail.splitlines()]
        return "\n".join(lines)


def _hello_client_id(sock_path: str) -> int | None:
    """Open a bare socket, read the hello line, return its ``client_id``.

    Deliberately does NOT use the SDK: `client_id` is a broker-assigned
    identity (D-038) and the point is to read what the broker actually
    put on the wire, not what a client library chose to keep. Returns
    ``None`` if the field is absent or the line doesn't parse, so the
    caller's check fails honestly rather than raising.
    """
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    try:
        s.settimeout(2.0)
        s.connect(sock_path)
        buf = b""
        while b"\n" not in buf and len(buf) < 4096:
            chunk = s.recv(256)
            if not chunk:
                break
            buf += chunk
        value = json.loads(buf.split(b"\n", 1)[0].decode()).get("client_id")
        return value if isinstance(value, int) else None
    except (OSError, ValueError):
        return None
    finally:
        s.close()


def _drain_broker(proc: subprocess.Popen[bytes]) -> str:
    """Read the broker's merged stdout/stderr, then close the pipe.

    The broker is spawned with ``stderr`` folded into ``stdout`` into a
    ``PIPE``. Call this only after the process has exited so ``read()``
    returns the full buffer + EOF rather than blocking. Never raises — a
    diagnostics helper must not mask the failure it exists to explain.
    Closing the pipe here also settles the ``ResourceWarning`` that
    ``filterwarnings = ["error"]`` would otherwise turn into a spurious
    test failure (Python 3.13 surfaces the orphaned FileIO as a
    ``PytestUnraisableExceptionWarning``).
    """
    if proc.stdout is None:
        return ""
    try:
        return proc.stdout.read().decode("utf-8", "replace")
    except (OSError, ValueError):
        return "(failed to read broker output)"
    finally:
        try:
            proc.stdout.close()
        except OSError:
            pass


def run_e2e() -> E2EResult:
    if not SERVER.exists():
        msg = f"broker binary not built at {SERVER}. Build it: `cd hrb-code && zig build`."
        print(f"FAIL: {msg}", file=sys.stderr)
        return E2EResult(2, failed_checks=[msg])

    if Path(SOCK).exists():
        Path(SOCK).unlink()

    proc = subprocess.Popen(  # noqa: S603 — argv-form, trusted path (SC-02)
        [str(SERVER)],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )
    # Wait until the broker ACCEPTS connections (connect-probe), not merely
    # until the socket file exists — a Unix socket appears at bind() but
    # refuses connects until listen(), so an existence check races under load.
    try:
        _broker.wait_for_socket(SOCK, 5.0)
    except TimeoutError:
        print("FAIL: broker never started accepting connections", file=sys.stderr)
        proc.kill()
        proc.wait(timeout=2)
        # Surface WHY it never accepted — a startup crash prints its
        # reason to the broker's own stdout/stderr, which we now capture.
        return E2EResult(
            1,
            failed_checks=["broker never started accepting connections"],
            broker_exit=proc.returncode,
            broker_output=_drain_broker(proc),
        )

    failed_checks: list[str] = []
    broker_output = ""
    broker_exit: int | None = None

    def check(name: str, cond: bool) -> None:
        status = "PASS" if cond else "FAIL"
        print(f"  {status}  {name}")
        if not cond:
            failed_checks.append(name)

    try:
        # Tier-0 #2 layer-1 (U3 closure): cap_id is no longer a
        # caller-supplied constant — the broker mints a CSPRNG-drawn
        # id and announces it via the hello handshake (Client.connect
        # consumes the hello and stores the id internally).
        c = Client.connect(SOCK, client_id=1)
        check(
            "cap_id from handshake is high-entropy (Tier-0 #2 layer-1)",
            isinstance(c._cap, int) and c._cap > 2**32,
        )

        # 1. list — Python asks Zig to enumerate; bytes over the socket.
        # We scope to `hw:/test/**` so the test is stable across hosts
        # (the broker also serves real PCI + USB; those vary per host).
        devs = c.list("hw:/test/**")
        # Assert the exact seeded set (server.zig seeds dev0, dev1, and the
        # G6 adversarial `edge_attrs` fixture). A bare count silently drifted
        # once already — an added/removed seed must force a conscious update.
        check(
            "list returns the broker's 3 seeded test devices",
            set(devs) == {"hw:/test/dev0", "hw:/test/dev1", "hw:/test/edge_attrs"},
        )
        check("list contains the seeded test urn", "hw:/test/dev0" in devs)

        # 2. claim — Python claims a device through the Zig lease engine.
        #    Using the Lease context manager (the canonical shape from recipe §3).
        target = "hw:/test/dev0"
        with c.claim(target, "exclusive") as lease:
            check(
                "claim returns a lease id",
                isinstance(lease.lease_id, int) and lease.lease_id > 0,
            )

            # 3. raw — Python obtains the raw channel. The broker
            #    SCM_RIGHTS-passes a real fd (a memfd in this test) so the
            #    SDK can do byte-I/O on the device without going back
            #    through the broker (G4 — owned access is broker-independent).
            r = lease.raw()
            check("raw channel granted to the holder", r.get("raw") is True)
            fd = r.get("fd")
            check(
                "raw response carries an SCM_RIGHTS fd (int, ≥0)",
                isinstance(fd, int) and fd >= 0,
            )
            if isinstance(fd, int) and fd >= 0:
                # The received fd is a real, usable Linux file descriptor.
                # Closing it doesn't affect the broker's copy; the broker's
                # close doesn't affect ours (Part 7.4 dividend).
                stat = os.fstat(fd)
                check(
                    "fstat on received fd succeeds (it's a real OS handle)",
                    stat.st_size >= 0,
                )
                os.close(fd)

            # 4. double-exclusive — Zig refuses; surfaces as a typed exception.
            try:
                c.claim(target, "exclusive")
                check("second exclusive claim is refused", False)
            except ConflictError:
                check(
                    "second exclusive claim refused (Conflict surfaced as typed exception)",
                    True,
                )

        # 5. After scope exit the lease was released; re-claim must work.
        with c.claim(target, "exclusive") as lease2:
            check(
                "re-claim after context-managed release succeeds",
                lease2.lease_id > 0,
            )

        # 6. deadline_ms flows through the boundary.
        with c.claim("hw:/test/dev1", "exclusive", deadline_ms=5000) as lease3:
            check(
                "claim with deadline_ms succeeds against a fast backend",
                lease3.lease_id > 0,
            )

        # ---- USB #2 + SDK-02 typed half: inspect() ------------------------
        # `Client.inspect()` returns a typed ResourceDetail dataclass
        # (FEEDBACK SDK-02 typed half); the legacy dict shape is on
        # `inspect_raw()`.
        detail = c.inspect("hw:/test/dev0")
        check(
            "inspect returns typed ResourceDetail with the urn",
            isinstance(detail, ResourceDetail) and detail.urn == "hw:/test/dev0",
        )
        check(
            "inspect carries typed family + kind + fidelity",
            detail.family is Family.CLAIMABLE
            and detail.kind in (Kind.USB_INTERFACE, Kind.PCIE_FUNCTION)
            and detail.fidelity is FieldFidelity.LIVE,
        )
        # Escape hatch: inspect_raw() returns the wire dict.
        detail_raw = c.inspect_raw("hw:/test/dev0")
        check(
            "inspect_raw returns the un-projected wire dict",
            isinstance(detail_raw, dict)
            and detail_raw.get("urn") == "hw:/test/dev0"
            and "family" in detail_raw,
        )

        # ---- SDK-05: capabilities() returns typed BackendCapability ------
        caps = c.capabilities()
        check(
            "capabilities returns typed BackendCapability records",
            len(caps) >= 1 and all(isinstance(b, BackendCapability) for b in caps),
        )
        scope_prefixes = {b.scope_prefix for b in caps}
        check(
            "capabilities surfaces all three workers (pci / usb / test)",
            {"hw:/pci/", "hw:/usb/", "hw:/test/"}.issubset(scope_prefixes),
        )
        test_worker = next(b for b in caps if b.scope_prefix == "hw:/test/")
        check(
            "the fake test worker reports `full` mode",
            test_worker.mode == "full",
        )

        # ---- SDK-05 escape hatch: capabilities_raw still returns dicts ---
        caps_raw = c.capabilities_raw()
        check(
            "capabilities_raw returns the un-projected wire dicts",
            (
                len(caps_raw) == len(caps)
                and all(isinstance(b, dict) for b in caps_raw)
                and "scope_prefix" in caps_raw[0]
            ),
        )

        # ---- SDK-03: list_leases() — who-holds-what view -----------------
        check("list_leases empty before any claim", c.list_leases() == [])
        with c.claim("hw:/test/dev0", ClaimMode.EXCLUSIVE) as held:
            leases = c.list_leases()
            check("list_leases sees the held lease", len(leases) == 1)
            r = leases[0]
            check(
                "list_leases row is a typed LeaseRecord",
                isinstance(r, LeaseRecord),
            )
            check(
                "lease record carries correct urn/mode/id",
                r.urn == "hw:/test/dev0"
                and r.lease_id == held.lease_id
                and r.mode is ClaimMode.EXCLUSIVE,
            )
            check(
                "lease record has a monotonic granted_at_ms timestamp",
                isinstance(r.granted_at_ms, int) and r.granted_at_ms > 0,
            )
            # Scope filter: narrow to USB only — the test-bus lease is excluded.
            usb_only = c.list_leases("hw:/usb/**")
            check(
                "list_leases scope filter narrows correctly",
                usb_only == [],
            )
        check(
            "list_leases empty again after the lease is released",
            c.list_leases() == [],
        )

        # ---- TOOLS-02: Client.doctor() — broker preconditions ------------
        findings = c.doctor()
        codes = [f.code for f in findings]
        # The engine emits a stable, ordered set of check codes.
        # R3 (2026-05-27): added USBFS_DEVNODE_PRESENT +
        # USBFS_DEVNODE_OPENABLE as USB worker substrate rows.
        expected_codes = {
            "SOCKET_REACHABLE",
            "PCI_SYSFS_READABLE",
            "USB_SYSFS_READABLE",
            "USBFS_DEVNODE_PRESENT",
            "USBFS_DEVNODE_OPENABLE",
            "VFIO_DEVICE_PRESENT",
            "IOMMU_PRESENT",
            "UDEV_RULE_INSTALLED",
            "BACKEND_CAPABILITIES",
        }
        check(
            "doctor returns all expected finding codes (R3: 9 total)",
            set(codes) == expected_codes,
        )
        check(
            "doctor findings are typed DoctorFinding records",
            all(isinstance(f, DoctorFinding) for f in findings),
        )
        sock_finding = next(f for f in findings if f.code == "SOCKET_REACHABLE")
        check(
            "broker socket check is OK (we're connected through it)",
            sock_finding.severity is DoctorSeverity.OK,
        )
        # With a live broker reachable, the backend-capabilities check fires
        # against the real wire op rather than returning DEFERRED.
        backend_finding = next(f for f in findings if f.code == "BACKEND_CAPABILITIES")
        check(
            "doctor BACKEND_CAPABILITIES check ran (not deferred)",
            backend_finding.severity is not DoctorSeverity.DEFERRED,
        )

        # ---- USB #4: drain_events() — non-blocking event poll ------------
        events = c.drain_events()
        check(
            "drain_events returns a list (empty on a quiet bus)",
            isinstance(events, list),
        )

        # ---- USB #8: control() — typed device-state verbs ----------------
        # The fake test backend has no `control` slot — the broker honestly
        # returns Unsupported (G2). The error round-trips as typed
        # UnsupportedError, not a generic MgfHrbError.
        try:
            c.control("hw:/test/dev0", ControlVerb.SET_AUTHORIZED, value=True)
            check("control on fake backend → Unsupported", False)
        except UnsupportedError:
            check(
                "control on fake backend honestly returns Unsupported (G2)",
                True,
            )

        c.close()

        # ---- Part 7.1 multi-client constellation: two concurrent ---------
        # Open two clients simultaneously and prove the broker serves
        # both without serial-accept deadlock. This is the property
        # that unblocks `monitor_fd`'s dedicated connection (SDK-01).
        c_a = Client.connect(SOCK)
        c_b = Client.connect(SOCK)
        try:
            # D-039 / CAP-01 — each connection gets its OWN capability,
            # bound to its peer at mint. This assertion is inverted from
            # what it used to be: the broker previously minted ONE root cap
            # and had every connection bind to it, which is precisely why a
            # second PROCESS could never connect.
            check(
                "each connection gets its own capability (CAP-01)",
                c_a._cap != c_b._cap and c_a._cap > 2**32 and c_b._cap > 2**32,
            )
            # c_a holds a lease; c_b sees it via list_leases.
            with c_a.claim("hw:/test/dev0", ClaimMode.EXCLUSIVE) as held:
                leases_from_b = c_b.list_leases("hw:/test/**")
                check(
                    "c_b sees c_a's lease via list_leases (cross-connection visibility)",
                    len(leases_from_b) == 1 and leases_from_b[0].lease_id == held.lease_id,
                )
                # c_b's claim on same URN is refused.
                try:
                    with c_b.claim("hw:/test/dev0", ClaimMode.EXCLUSIVE):
                        check("c_b second claim on same URN", False)
                except ConflictError:
                    check(
                        "c_b second exclusive claim refused with Conflict",
                        True,
                    )
            # After c_a releases, c_b can claim.
            with c_b.claim("hw:/test/dev0", ClaimMode.EXCLUSIVE) as held_b:
                check(
                    "c_b can claim after c_a's release (multi-client handoff)",
                    held_b.lease_id > 0,
                )
        finally:
            c_a.close()
            c_b.close()

        # ---- CORE-02: release is authorized against the connection -------
        # `release` carries no caller and no cap_id on the wire, so before
        # this the engine had no identity to check and possession of a lease
        # id was full authority — over a socket, from a guessable integer,
        # with no capability presented. Verified end to end because the
        # dispatch-seam tests cannot show that the WIRE op reaches the check
        # with the connection's identity attached.
        c_owner = Client.connect(SOCK)
        c_other = Client.connect(SOCK)
        try:
            owned = c_owner.claim("hw:/test/dev1", ClaimMode.EXCLUSIVE)
            try:
                c_other.release("hw:/test/dev1", owned.lease_id)
                stole = True
            except NotPermittedError:
                stole = False
            check("a non-holder cannot release another's lease (CORE-02)", not stole)
            check(
                "the refused release left the lease standing",
                any(row.lease_id == owned.lease_id for row in c_owner.list_leases("hw:/test/**")),
            )
            # An unknown id must stay a silent no-op for anyone: the check
            # sits after the not-found return precisely so the error cannot
            # be used to enumerate which lease ids are live.
            try:
                c_other.release("hw:/test/dev1", 999_999)
                unknown_ok = True
            except MgfHrbError:
                unknown_ok = False
            check("an unknown lease id stays idempotent, not an oracle", unknown_ok)
            # Positive control: the real holder is still able to release.
            owned.release()
            check(
                "the holder can still release its own lease",
                not c_owner.list_leases("hw:/test/dev1**"),
            )
        finally:
            c_owner.close()
            c_other.close()

        # ---- D-038 / G3: connection death reclaims the lease -------------
        # The guarantee D-011 states outright — "a lease exists only while
        # its holder is provably alive… ghost ownership is structurally
        # impossible". Before FEEDBACK CORE-01 this was true of the lease
        # ENGINE and false of the running broker: `Facade.holderDied` had
        # no production caller, so a client that died holding an exclusive
        # lease stranded the device until the broker restarted.
        #
        # Shape: two connections from THIS process (a second process cannot
        # connect while CAP-01 stands — the root cap is bound to the first
        # peer). `c_dead` claims and is then closed with no protocol
        # release, which is byte-identical, from the broker's read loop, to
        # a SIGKILL. `c_live` is the observer.
        c_live = Client.connect(SOCK)
        try:
            c_dead = Client.connect(SOCK)
            reclaim_target = "hw:/test/dev1"
            dead_lease = c_dead.claim(reclaim_target, ClaimMode.EXCLUSIVE)
            check(
                "a lease is held before the holder dies",
                any(row.urn == reclaim_target for row in c_live.list_leases("hw:/test/**")),
            )

            # The control that makes the assertion below mean something:
            # active_connections must DROP, proving the broker ran the dead
            # connection's teardown. Without it, "the lease is gone" could
            # equally mean "we looked too early".
            conns_before = c_live.state_snapshot().active_connections
            c_dead.close()  # no release sent — EOF, exactly like a crash

            reclaimed = False
            conns_after = conns_before
            for _ in range(50):  # bounded: ≤5s, polled at 100ms
                time.sleep(0.1)
                snap = c_live.state_snapshot()
                conns_after = snap.active_connections
                if not any(row.urn == reclaim_target for row in c_live.list_leases("hw:/test/**")):
                    reclaimed = True
                    break

            check(
                "broker observed the death (active_connections dropped)",
                conns_after < conns_before,
            )
            check(
                "connection death reclaimed the dead holder's lease (D-038/G3)",
                reclaimed,
            )
            # The consequence that actually matters to an operator: the
            # device is usable again, not stranded until a broker restart.
            #
            # ConflictError is caught rather than allowed to propagate
            # BECAUSE it is the exact symptom of the regression this block
            # guards. Letting it escape would abort the run and suppress
            # every check after this one — a test that hides its
            # successors when it fires is worth less than the bug it
            # catches. Verified by seeded sabotage: with the reclamation
            # defer removed, this reports as a clean FAIL and the suite
            # continues.
            try:
                with c_live.claim(reclaim_target, ClaimMode.EXCLUSIVE) as reborn:
                    check(
                        "the reclaimed device can be claimed again (not stranded)",
                        reborn.lease_id > dead_lease.lease_id,
                    )
            except ConflictError:
                check(
                    "the reclaimed device can be claimed again (not stranded)",
                    False,
                )
        finally:
            c_live.close()

        # ---- D-038: the broker assigns each connection its own identity --
        # `client_id` in the hello is what lets a multi-operator console say
        # "yours" rather than merely listing holders. Distinctness is the
        # property that makes per-connection reclamation well-defined.
        id_a = _hello_client_id(SOCK)
        id_b = _hello_client_id(SOCK)
        check("hello carries a broker-assigned client_id", id_a is not None)
        check(
            "each connection gets a distinct, increasing client_id",
            id_a is not None and id_b is not None and id_b > id_a,
        )

        # ---- D-039 / CAP-01: reconnect gets a fresh capability -----------
        # Reconnecting from the same peer must succeed AND receive a new
        # capability, not the previous connection's. The old scheme bound
        # one shared cap to the first {uid, pid} and never released it, so
        # a restarted client — new pid — was refused NotPermitted by a
        # perfectly healthy broker. That was the defect an operator met
        # every second launch.
        prior_cap = c._cap
        with Client.connect(SOCK) as c2:
            check(
                "reconnect succeeds and gets a FRESH capability (CAP-01)",
                c2._cap != prior_cap and c2._cap > 2**32,
            )
            # The previous connection's capability was revoked when it
            # closed, so presenting it now must be refused. This is the
            # half that keeps per-connection caps from being a mere
            # renaming: authority genuinely ends with the connection.
            stale_payload = (
                '{"op":"claim","urn":"hw:/test/dev0",'
                '"mode":"exclusive","holder":1,'
                f'"cap_id":{prior_cap}}}\n'
            ).encode()
            c2._t._sock.sendall(stale_payload)  # type: ignore[attr-defined]
            stale_resp = json.loads(c2._t._recv_line())  # type: ignore[attr-defined]
            check(
                "a closed connection's capability no longer works (revoked)",
                stale_resp.get("ok") is False and stale_resp.get("err") == "NotPermitted",
            )

        # ---- CAP-01: a SEPARATE PROCESS can connect ----------------------
        # The headline property, and the one no in-process check can reach:
        # the old binding was on {uid, PID}, so every assertion made from
        # this process passed whether the bug existed or not. A second
        # process was refused NotPermitted at the handshake — which is why
        # multi-operator was impossible and why relaunching the GUI failed
        # against a healthy broker.
        #
        # Runs a child interpreter that connects, claims, and reports. The
        # child gets its own pid, so it is exactly the case that used to
        # fail.
        # Same interpreter, inherited environment — so `mgf.hrb` resolves
        # exactly as it does here. The only thing that differs is the pid,
        # which is the whole point.
        child_src = (
            "from mgf.hrb import Client;"
            f"c = Client.connect({SOCK!r}, client_id=7);"
            "lease = c.claim('hw:/test/edge_attrs', 'exclusive');"
            "print('CHILD_OK', c._cap, lease.lease_id);"
            "c.close()"
        )
        child = subprocess.run(  # noqa: S603 - fixed argv, no shell
            [sys.executable, "-c", child_src],
            capture_output=True,
            text=True,
            timeout=20,
        )
        child_ok = child.returncode == 0 and "CHILD_OK" in child.stdout
        check("a separate PROCESS can connect and claim (CAP-01)", child_ok)
        if child_ok:
            child_cap = int(child.stdout.split()[1])
            check(
                "the second process got its own capability, not ours",
                child_cap != c._cap,
            )
        else:
            check("the second process got its own capability, not ours", False)
            print(f"    child stderr: {child.stderr.strip()[:400]}", file=sys.stderr)

        # ---- Tier-0 #2 layer-1: a forged cap_id is refused ---------------
        # Hand-craft a wire request bypassing the SDK to prove the broker
        # rejects an unauthenticated cap_id (the U3 closure live).
        with Client.connect(SOCK) as c3:
            forged_id = 12345  # unguessable real id is 64-bit random
            payload = (
                '{"op":"claim","urn":"hw:/test/dev0",'
                '"mode":"exclusive","holder":1,'
                f'"cap":{forged_id}}}\n'
            ).encode()
            c3._t._sock.sendall(payload)  # type: ignore[attr-defined]
            resp_line = c3._t._recv_line()  # type: ignore[attr-defined]
            import json as _json

            forged_resp = _json.loads(resp_line)
            check(
                "forged cap_id refused with NotPermitted (Tier-0 #2 U3 closure)",
                forged_resp.get("ok") is False and forged_resp.get("err") == "NotPermitted",
            )
    except MgfHrbError as exc:
        print(f"FAIL: unexpected broker error: {exc}", file=sys.stderr)
        failed_checks.append(f"unexpected broker error: {exc}")
    finally:
        proc.send_signal(signal.SIGTERM)
        try:
            proc.wait(timeout=2)
        except subprocess.TimeoutExpired:
            proc.kill()
            proc.wait(timeout=2)
        broker_exit = proc.returncode
        # Read the broker's merged stdout/stderr BEFORE closing the pipe.
        # On a red e2e this is the ONLY record of the broker's own reason
        # for exit-1 (a panic, a wire error, a backend-init failure); the
        # pre-fix teardown closed the pipe unread, so the failure bundle's
        # stdout/stderr came back empty. `_drain_broker` also closes the
        # pipe (settling the Python 3.13 unraisable-FileIO warning).
        broker_output = _drain_broker(proc)
        if Path(SOCK).exists():
            Path(SOCK).unlink()

    print()
    if not failed_checks:
        print("  ALL E2E CHECKS PASSED — Python↔Zig over a socket, zero FFI")
        return E2EResult(0, broker_exit=broker_exit, broker_output=broker_output)
    print(f"  {len(failed_checks)} CHECK(S) FAILED")
    # Echo the broker's own output so a direct `python tests/e2e/test_e2e.py`
    # run shows the reason too (pytest gets it via E2EResult.report()).
    if broker_output.strip():
        print("\n--- broker stdout/stderr (merged) ---", file=sys.stderr)
        print(broker_output.rstrip(), file=sys.stderr)
    print(f"--- broker exit: {broker_exit} ---", file=sys.stderr)
    return E2EResult(
        1,
        failed_checks=failed_checks,
        broker_exit=broker_exit,
        broker_output=broker_output,
    )


def test_e2e() -> None:
    """Pytest entry point. Skips if the broker binary isn't built."""
    if not SERVER.exists():
        import pytest

        pytest.skip(f"Broker binary not built at {SERVER}; run `zig build` in hrb-code/")
    result = run_e2e()
    # `result.report()` carries the failed check names, the broker exit
    # code, and the broker's captured stdout/stderr. It lands in the
    # AssertionError message → junit `<failure>` → mtest failure bundle,
    # the one channel that survives (junit_logging=no drops captured
    # stdout) — so a red e2e now explains itself instead of just "exit 1".
    assert result.exit_code == 0, result.report()


def test_e2e_result_report_surfaces_broker_reason() -> None:
    """Regression guard for the diagnosability fix (no broker required).

    A failed ``E2EResult.report()`` must name the failed checks, the
    broker exit code, AND the broker's captured stdout/stderr — the trio
    that was missing from the mtest failure bundle before this fix.
    """
    result = E2EResult(
        exit_code=1,
        failed_checks=["list returns the broker's 2 test devices"],
        broker_exit=-6,
        broker_output="mgf-hrb broker listening on /tmp/x\nthread panic: reached unreachable\n",
    )
    report = result.report()
    assert "exit 1" in report
    assert "list returns the broker's 2 test devices" in report
    assert "broker exit code: -6" in report
    assert "thread panic: reached unreachable" in report

    # The empty-output path names the absence explicitly — never a blank gap.
    quiet = E2EResult(exit_code=1, failed_checks=["x"], broker_exit=0, broker_output="")
    assert "(broker emitted nothing" in quiet.report()


if __name__ == "__main__":
    raise SystemExit(run_e2e().exit_code)
