"""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 os
import signal
import subprocess
import sys
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,
    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"


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

    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()
        return 1

    failures = 0

    def check(name: str, cond: bool) -> None:
        nonlocal failures
        status = "PASS" if cond else "FAIL"
        print(f"  {status}  {name}")
        if not cond:
            failures += 1

    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/**")
        check("list returns the broker's 2 test devices", len(devs) == 2)
        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:
            check(
                "two concurrent connections share the same broker cap (bind idempotent)",
                c_a._cap == c_b._cap,
            )
            # 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()

        # ---- Tier-0 #2 layer-1: same-peer reconnect is idempotent --------
        # The first connect bound the root cap to this process's
        # SO_PEERCRED; reconnecting from the same peer (same uid+pid)
        # must succeed without "wrong identity" rejection.
        with Client.connect(SOCK) as c2:
            check(
                "same-peer reconnect succeeds (bindTo is idempotent)",
                c2._cap == c._cap,
            )
            # The cap_id is stable across reconnects — the broker minted
            # one root cap at startup, not one per connection.
            check(
                "cap_id is broker-stable across reconnects",
                c2._cap == c._cap and c2._cap > 2**32,
            )

        # ---- 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)
        failures += 1
    finally:
        proc.send_signal(signal.SIGTERM)
        try:
            proc.wait(timeout=2)
        except subprocess.TimeoutExpired:
            proc.kill()
        # Close the captured stdout pipe (subprocess.PIPE leaves it open
        # until GC; Python 3.13's stricter unraisable-exception flow
        # surfaces the orphaned FileIO as a PytestUnraisableExceptionWarning
        # that fails the test even though the e2e checks all passed).
        if proc.stdout is not None:
            proc.stdout.close()
        if Path(SOCK).exists():
            Path(SOCK).unlink()

    print()
    if failures == 0:
        print("  ALL E2E CHECKS PASSED — Python↔Zig over a socket, zero FFI")
        return 0
    print(f"  {failures} CHECK(S) FAILED")
    return 1


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/")
    rc = run_e2e()
    assert rc == 0, f"E2E checks failed (exit {rc})"


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