"""Regression pin for J4 (wave-2 follow-up, presentation-layer round).

**The bug.** ``_Transport`` made bare ``socket.sendall`` and
``socket.recv`` / ``recvmsg`` calls. If the broker died (or
abruptly closed the socket) mid-call, the kernel surfaced
``BrokenPipeError`` / ``ConnectionResetError`` — both ``OSError``
subclasses, NEITHER a subclass of ``MgfHrbError``. A consumer
doing the canonical typed catch::

    try:
        client.list("hw:/**")
    except MgfHrbError:
        ...  # surfaces to UI / retry

would silently miss the broker-disconnect path. In a Qt GUI
that means the exception escapes the slot + crashes the event
loop; in a CLI it means a raw traceback instead of the operator-
facing "broker disconnected" message.

**The fix.** Each of the three transport I/O sites (``sendall``,
``recv``, ``recvmsg``) wraps the call in
``try/except OSError → raise MgfHrbError(...) from exc``. The
``from exc`` preserves chaining per EH-03; the logger.warning
preserves observability per LG-05.

**This pin.** Constructs a transport over a socketpair. For
each of the three I/O surfaces, closes the peer end (the broker
side) before the call so the kernel raises the corresponding
``OSError`` subclass. Asserts that:

  1. ``MgfHrbError`` propagates (not ``OSError``).
  2. ``__cause__`` is set to the underlying ``OSError`` so a
     debugger can still see the kernel-level reason.
  3. The original ``OSError`` is reachable via ``.__cause__``.

Marker: ``concurrency`` because it exercises a real socketpair
even though no threads are involved (the "two-end socket" shape
is the relevant federation marker — no plain-unit alternative
applies cleanly).
"""

from __future__ import annotations

import socket
from typing import Any

import pytest

from mgf.hrb._client import _Transport
from mgf.hrb.exceptions import MgfHrbError


@pytest.fixture
def transport_with_dead_peer() -> Any:
    """Build a transport whose peer-end is closed BEFORE any I/O.
    Sendall / recv / recvmsg will all trip BrokenPipeError /
    ConnectionResetError on first use — exactly the "broker died"
    shape."""
    a, b = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
    b.close()  # peer (broker) is gone before the SDK tries to talk
    t = _Transport(a)
    yield t
    a.close()


def test_sendall_oserror_becomes_mgf_hrb_error(
    transport_with_dead_peer: _Transport,
) -> None:
    """The first I/O the SDK does is `sendall`. With the peer
    closed, the kernel returns EPIPE → BrokenPipeError. Pre-J4
    that propagated raw. Post-J4 it surfaces as MgfHrbError with
    BrokenPipeError chained via __cause__."""
    with pytest.raises(MgfHrbError) as exc_info:
        transport_with_dead_peer._call_locked(op="list", scope="hw:/**")

    assert "broker connection lost" in str(exc_info.value).lower(), (
        f"unexpected message: {exc_info.value}"
    )
    # Chaining preserved per EH-03.
    assert exc_info.value.__cause__ is not None, (
        "MgfHrbError must chain the original OSError via 'from exc'"
    )
    assert isinstance(exc_info.value.__cause__, OSError), (
        f"__cause__ should be the OSError subclass; got {type(exc_info.value.__cause__).__name__}"
    )


class _FakeSock:
    """Minimal duck-typed socket whose recv / recvmsg / sendall
    can be programmed to succeed once, then raise on next call.
    Real socket objects have read-only attributes so monkeypatch
    can't replace methods — this fake stands in for transport
    tests that need precise OSError injection.

    `close()` is a no-op so the _Transport teardown path doesn't
    bomb when the test re-uses the fake across assertions."""

    def __init__(
        self,
        *,
        recv_raises: OSError | None = None,
        recvmsg_raises: OSError | None = None,
    ) -> None:
        self._recv_raises = recv_raises
        self._recvmsg_raises = recvmsg_raises
        self.sent: list[bytes] = []

    def sendall(self, data: bytes) -> None:
        self.sent.append(data)

    def recv(self, *_args: Any, **_kwargs: Any) -> bytes:
        if self._recv_raises is not None:
            raise self._recv_raises
        return b""

    def recvmsg(self, *_args: Any, **_kwargs: Any) -> tuple[Any, ...]:
        if self._recvmsg_raises is not None:
            raise self._recvmsg_raises
        return (b"", [], 0, None)

    def close(self) -> None:
        return None


def test_recv_oserror_becomes_mgf_hrb_error() -> None:
    """Force the second I/O (recv) to raise ConnectionResetError
    after sendall succeeded. Pre-J4 the raw ConnectionResetError
    would propagate; post-J4 MgfHrbError."""
    fake = _FakeSock(recv_raises=ConnectionResetError(104, "Connection reset by peer"))
    transport = _Transport(fake)  # type: ignore[arg-type]

    with pytest.raises(MgfHrbError) as exc_info:
        transport._call_locked(op="list", scope="hw:/**")

    assert isinstance(exc_info.value.__cause__, ConnectionResetError), (
        f"__cause__ should be ConnectionResetError; got {type(exc_info.value.__cause__).__name__}"
    )
    assert "broker connection lost" in str(exc_info.value).lower()


def test_recvmsg_oserror_becomes_mgf_hrb_error() -> None:
    """`raw` ops use recvmsg. Force it to raise BrokenPipeError;
    assert MgfHrbError wraps it (the recvmsg path needs its own
    pin because it's a different code path from `recv`)."""
    fake = _FakeSock(recvmsg_raises=BrokenPipeError(32, "Broken pipe"))
    transport = _Transport(fake)  # type: ignore[arg-type]

    with pytest.raises(MgfHrbError) as exc_info:
        transport._call_locked(op="raw", urn="hw:/test/x")

    assert isinstance(exc_info.value.__cause__, BrokenPipeError), (
        f"__cause__ should be BrokenPipeError; got {type(exc_info.value.__cause__).__name__}"
    )


def test_typed_catch_covers_broker_disconnect(
    transport_with_dead_peer: _Transport,
) -> None:
    """The high-level invariant the J4 fix protects: any consumer
    doing `try: ... except MgfHrbError: ...` MUST catch the
    broker-disconnect path. Pre-J4 this test fails because
    BrokenPipeError escapes the typed-catch; post-J4 it's caught."""
    caught_typed = False
    try:
        transport_with_dead_peer._call_locked(op="list", scope="hw:/**")
    except MgfHrbError:
        caught_typed = True
    except OSError:
        pytest.fail(
            "J4 regression: bare OSError escaped the typed exception "
            "family. Consumers catching MgfHrbError will miss the "
            "broker-disconnect path."
        )
    assert caught_typed, "typed MgfHrbError was not caught"
