"""Regression pin for cluster-G4 (review wave 2, commit ``6320ac5``).

**The bug.** ``_Transport._call_locked`` for the ``raw`` op used
``recvmsg`` to receive the response line + an SCM_RIGHTS fd in a
single call. If the broker sent malformed bytes after the cmsg
(broker bug, partial-shutdown, anything that produces non-JSON),
``json.loads`` raised — but the kernel-installed fd had already
landed in our process's fd table. Pre-G4, that fd leaked silently;
under churn the client would eventually exhaust its fd ulimit.

**The fix.** A ``fd_consumed`` flag flips ``True`` only when the
response is successfully parsed AND ownership of the fd is
transferred into the returned dict. A finally block closes the
fd otherwise. The structural guarantee is: every exit path
(success, MgfHrbError, JSONDecodeError, anything else) either
consumes the fd OR closes it.

**This pin.** Constructs a Transport, monkeypatches
``_recv_line_with_optional_fd`` to return malformed JSON +
a real fd (from ``os.pipe()``). Calls ``_call_locked`` with
``op="raw"`` so the fd-receiving branch runs. Expects either
``json.JSONDecodeError`` or ``MgfHrbError`` to propagate; asserts
the returned fd was closed via the finally block.

Pre-fix this test fails because the fd remains open after the
exception. Post-fix the fd is closed; ``os.fstat`` on it raises
``OSError`` with ``EBADF`` — the canonical signal that we don't
own that descriptor any more.
"""

from __future__ import annotations

import errno
import json
import os
import socket
from typing import Any

import pytest

from mgf.hrb._client import _Transport


@pytest.fixture
def transport() -> Any:
    """Build a transport over a socketpair; peer end stays open so
    `sendall` doesn't trip on a broken pipe. Both ends close in
    teardown so ResourceWarning (filterwarnings=error) stays
    quiet."""
    a, b = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
    t = _Transport(a)
    yield t
    a.close()
    b.close()


def _is_fd_closed(fd: int) -> bool:
    """True iff the fd is no longer in our process's fd table."""
    try:
        os.fstat(fd)
    except OSError as exc:
        return exc.errno == errno.EBADF
    return False


def test_fd_is_closed_when_response_is_malformed_json(
    transport: _Transport,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """The G4 regression pin. Malformed wire bytes → json.loads
    raises → the fd MUST be closed by the finally block, not leaked."""
    read_end, write_end = os.pipe()
    os.close(write_end)  # only need the read end for fd-ownership testing

    def _fake_recv(self: _Transport) -> tuple[bytes, int]:
        del self
        return (b"this is not valid json\n", read_end)

    monkeypatch.setattr(
        _Transport,
        "_recv_line_with_optional_fd",
        _fake_recv,
    )

    # Pre-fix: json.JSONDecodeError escapes + fd leaks.
    # Post-fix: same exception escapes + finally closes the fd.
    with pytest.raises(json.JSONDecodeError):
        transport._call_locked(op="raw", urn="hw:/test/x")

    assert _is_fd_closed(read_end), (
        f"G4 regression: fd {read_end} was leaked. The finally "
        f"block in _Transport._call_locked should have closed it "
        f"because json.loads raised before fd_consumed was set."
    )


def test_fd_is_closed_when_broker_returns_error_with_unexpected_fd(
    transport: _Transport,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Defensive sub-case: broker returns a typed error (ok:false)
    but somehow an fd arrived alongside it (broker bug). The fd
    MUST be closed; consumer sees only the MgfHrbError."""
    read_end, write_end = os.pipe()
    os.close(write_end)

    def _fake_recv(self: _Transport) -> tuple[bytes, int]:
        del self
        return (b'{"ok":false,"err":"NotFound"}\n', read_end)

    monkeypatch.setattr(
        _Transport,
        "_recv_line_with_optional_fd",
        _fake_recv,
    )

    from mgf.hrb.exceptions import NotFoundError

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

    assert _is_fd_closed(read_end), (
        f"G4 regression: fd {read_end} was leaked on a typed-error "
        f"path. The finally block should close it whenever "
        f"fd_consumed is False."
    )


def test_fd_is_consumed_on_successful_raw_response(
    transport: _Transport,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Contrapositive: the successful-path fd transfer still works.
    `{"ok":true,"fd":true}` + an fd → the dict contains the int fd;
    the fd MUST remain open (caller owns it now).

    The caller is responsible for closing the fd they were given;
    this test closes it explicitly at the end to keep the fd table
    tidy in CI."""
    read_end, write_end = os.pipe()
    os.close(write_end)

    def _fake_recv(self: _Transport) -> tuple[bytes, int]:
        del self
        return (b'{"ok":true,"fd":true}\n', read_end)

    monkeypatch.setattr(
        _Transport,
        "_recv_line_with_optional_fd",
        _fake_recv,
    )

    resp: dict[str, Any] = transport._call_locked(op="raw", urn="hw:/test/x")
    assert resp["fd"] == read_end, "fd should be transferred to the response dict (caller-owned)"
    assert not _is_fd_closed(read_end), (
        "successful raw response should NOT close the fd — caller owns it"
    )
    # Cleanup — caller's job in real code.
    os.close(read_end)
