"""Shared contract-test fixtures (F1 — consolidation, 2026-05-26).

Before this file, nine `tests/contract/test_wire_*.py` files
each carried a near-identical 30-45 line `raw_broker` fixture +
a 5-line `_rt` round-trip helper. The duplication grew so wide
that the comment "Same shape as the other `test_wire_*` files"
appeared verbatim in two of them. This conftest collapses them.

Fixtures:
  * ``raw_broker`` — spawns the broker subprocess, returns a raw
    socket (NOT a ``Client``), drains the hello line, tears
    down on exit. The common case for wire-shape pins that
    don't need the root cap.
  * ``raw_broker_with_cap`` — same plus the parsed cap_id from
    the hello, for ops behind the ``audit_read`` / ``steal`` /
    other cap gates.

Helpers (importable from this module):
  * ``rt(sock, req)`` — synchronous round-trip: send + recv one
    line + JSON-parse.

Federation-reuse note: this file + ``tests/_broker.py`` together
are the shared broker-spawn shape; each suite (contract, e2e,
stress, perf) adds its own conftest only when it needs suite-
specific knobs.
"""

from __future__ import annotations

import json
import socket
from typing import TYPE_CHECKING, Any

import pytest

from tests import _broker

if TYPE_CHECKING:
    import subprocess
    from collections.abc import Iterator


# Per-pytest-xdist-worker socket path (the shared `_broker` helper honours
# PYTEST_XDIST_WORKER). Evaluated once per worker process at import time, so
# parallel workers each target their own broker socket without collision.
# Imported by tests that talk wire-level outside the fixtures (e.g.
# `test_conn_cap.py`), so they hit the same socket the fixture's broker binds.
_SOCK_PATH = _broker.default_sock_path()


def _spawn_broker_and_connect() -> tuple[subprocess.Popen[bytes], socket.socket, int]:
    """Spawn the broker (bound to the per-worker `_SOCK_PATH` via the
    `MGF_HRB_SOCK` env override) + open a client connection.

    Returns (proc, sock, cap_id). Callers tear down via `_teardown`.

    Freshness first, same skip-vs-fail policy as the SDK-level `broker`
    fixture in `tests/conftest.py` (absent → skip, stale → loud fail).
    Until M-B B1 these raw-socket fixtures carried NO staleness check,
    so the whole raw contract suite would happily run against a broker
    built before the Zig change under test — the 2026-05-25 trap the
    guard exists to close, reopened for every `test_wire_*.py` file.
    A full-suite run was covered incidentally by
    `tests/smoke/test_broker_freshness.py`, but a targeted
    `pytest tests/contract/…` was not, and that is exactly how a
    contributor iterates on wire shape.
    """
    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)

    proc = _broker.spawn_broker(_SOCK_PATH)
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(_SOCK_PATH)
    buf = b""
    while b"\n" not in buf:
        buf += s.recv(4096)
    hello = json.loads(buf.split(b"\n", 1)[0])
    cap_id = int(hello["cap_id"])
    return (proc, s, cap_id)


def _teardown(proc: subprocess.Popen[bytes], s: socket.socket) -> None:
    """Close the client socket, then SIGTERM→KILL the broker + unlink the
    socket (delegated to the shared `_broker.teardown_broker`)."""
    try:
        s.close()
    except OSError:
        pass
    _broker.teardown_broker(proc, _SOCK_PATH)


@pytest.fixture
def raw_broker() -> Iterator[socket.socket]:
    """Spawn the broker subprocess + return a raw socket connected
    to it (NOT a ``Client``). The hello line is already drained.
    Teardown SIGTERMs the broker. Use when the wire test doesn't
    need the cap_id from the hello."""
    proc, s, _cap = _spawn_broker_and_connect()
    try:
        yield s
    finally:
        _teardown(proc, s)


@pytest.fixture
def raw_broker_with_cap() -> Iterator[tuple[socket.socket, int]]:
    """Spawn the broker + return ``(socket, root_cap_id)``. Use
    for ops behind a cap gate (``audit_read`` / ``steal`` / etc.):
    every cap-gated request needs ``cap_id`` in the body."""
    proc, s, cap = _spawn_broker_and_connect()
    try:
        yield (s, cap)
    finally:
        _teardown(proc, s)


def rt(sock: socket.socket, req: dict[str, Any]) -> dict[str, Any]:
    """Synchronous round-trip: send one JSON request line, recv +
    parse one JSON response line. The contract suite's universal
    request shape. Public so tests can import it."""
    sock.send((json.dumps(req) + "\n").encode())
    buf = b""
    while b"\n" not in buf:
        buf += sock.recv(65536)
    return json.loads(buf.split(b"\n", 1)[0])
