"""Regression pin for SDK-06 (mgf-hrb FEEDBACK.md, filed by hrb-app
2026-05-25).

**The bug.** ``Client.connect(path)`` blocked on ``_recv_line``
waiting for the broker's hello message. If the peer completed
``accept`` but never wrote the hello (DoS, partial test fake,
wedged broker), ``connect`` hung indefinitely — no timeout
fired. hrb-app's `tests/integration/test_launch.py` hit this
during the 0.39 SDK-adoption pass and spent ~10 minutes
bisecting before the cause was clear.

**The fix.** ``Client.connect`` accepts ``hello_deadline_ms=2000``
(kwarg-only, backward-compatible default). The deadline applies
to the socket via ``settimeout`` for the hello phase only;
restored to blocking mode after the hello completes. A trip
surfaces as ``MgfHrbError`` with a precise message naming the
deadline — typed catch, no hung process.

**This pin.** Spawns a tiny Unix-domain server thread that
``accept``s connections but never writes a hello. ``Client.connect``
against that server MUST raise ``MgfHrbError`` within the
deadline window. Without the fix, the test would hang until
the pytest timeout (30s) and fail noisily — the bug's exact
shape, just slower.

Three pins:

  1. Silent peer + default deadline (2000ms) → raises within
     ~3s with the canonical SDK-06 message.
  2. Silent peer + short deadline (200ms) → raises faster,
     proving the kwarg is honored.
  3. Successful hello → deadline doesn't fire; subsequent ops
     are NOT subject to the hello deadline (regression catch:
     forgetting ``settimeout(None)`` would break long-running
     ops).
"""

from __future__ import annotations

import json
import socket
import tempfile
import threading
import time
from contextlib import suppress
from pathlib import Path
from typing import Any

import pytest

from mgf.hrb import Client
from mgf.hrb.exceptions import MgfHrbError


class _SilentServer:
    """Unix-domain server that ``accept``s but never writes.

    The client's ``Client.connect`` will get the TCP-equivalent of
    "connection succeeded, peer is silent" — exactly the wedged-
    broker shape SDK-06 protects against.
    """

    def __init__(self, hello: bytes | None = None) -> None:
        # Use a tmpdir-scoped socket path so concurrent tests don't
        # collide; AF_UNIX path length cap (~108 chars) is fine
        # under tempfile.mkdtemp.
        self._tmpdir = tempfile.mkdtemp(prefix="sdk06-")
        self.path = str(Path(self._tmpdir) / "broker.sock")
        self._hello = hello  # if not None, write this on accept
        self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self._sock.bind(self.path)
        self._sock.listen(4)
        self._stop = False
        self._accepted: list[socket.socket] = []
        self._thread = threading.Thread(target=self._serve, daemon=True)
        self._thread.start()

    def _serve(self) -> None:
        self._sock.settimeout(0.1)  # poll for stop flag
        while not self._stop:
            try:
                conn, _addr = self._sock.accept()
            except (TimeoutError, OSError):
                continue
            self._accepted.append(conn)
            if self._hello is not None:
                with suppress(OSError):
                    conn.sendall(self._hello)

    def stop(self) -> None:
        self._stop = True
        # Wait briefly for the serve thread to wind down.
        self._thread.join(timeout=2.0)
        with suppress(OSError):
            self._sock.close()
        for conn in self._accepted:
            with suppress(OSError):
                conn.close()
        with suppress(OSError):
            Path(self.path).unlink()


@pytest.fixture
def silent_server() -> Any:
    """A server that accepts but never writes a hello."""
    srv = _SilentServer(hello=None)
    yield srv
    srv.stop()


@pytest.fixture
def hello_server() -> Any:
    """A server that writes a valid hello immediately on accept."""
    srv = _SilentServer(
        hello=json.dumps({"ok": True, "hello": "mgf-hrb", "cap_id": 42}).encode() + b"\n",
    )
    yield srv
    srv.stop()


def test_silent_peer_trips_default_deadline(silent_server: _SilentServer) -> None:
    """The core SDK-06 case: peer accepts but never writes the
    hello. Pre-fix Client.connect hangs forever; post-fix it
    raises MgfHrbError within the default 2000ms deadline."""
    t0 = time.monotonic()
    with pytest.raises(MgfHrbError) as exc_info:
        Client.connect(silent_server.path)
    elapsed = time.monotonic() - t0
    # Default deadline is 2000ms; allow some slack for thread
    # scheduling. Pre-fix this would hang until the pytest
    # timeout (30s).
    assert elapsed < 4.0, f"Client.connect took {elapsed:.1f}s — deadline not honored"
    assert "hello not received" in str(exc_info.value).lower(), (
        f"unexpected message: {exc_info.value}"
    )
    assert "2000ms" in str(exc_info.value), f"message should name the deadline: {exc_info.value}"
    # __cause__ chain preserved (EH-03).
    assert exc_info.value.__cause__ is not None, (
        "MgfHrbError should chain the timeout cause via 'from'"
    )


def test_short_deadline_is_honored(silent_server: _SilentServer) -> None:
    """The kwarg is plumbed through, not ignored. A 200ms
    deadline should trip ~10x faster than the default."""
    t0 = time.monotonic()
    with pytest.raises(MgfHrbError) as exc_info:
        Client.connect(silent_server.path, hello_deadline_ms=200)
    elapsed = time.monotonic() - t0
    assert elapsed < 1.0, f"200ms deadline took {elapsed:.2f}s — kwarg not honored"
    assert "200ms" in str(exc_info.value), (
        f"message should reflect the custom deadline: {exc_info.value}"
    )


def test_successful_hello_then_socket_returns_to_blocking(
    hello_server: _SilentServer,
) -> None:
    """The happy path with the new kwarg: hello arrives, connect
    succeeds, and the socket is restored to blocking mode (no
    inherited timeout that would cause subsequent ops to
    spuriously trip).

    Regression catch: forgetting ``s.settimeout(None)`` after a
    successful handshake would break long-running ops that don't
    pass a deadline_ms.
    """
    client = Client.connect(hello_server.path)
    try:
        # Internal access is fine for a regression pin — we're
        # asserting the post-condition the public API promises.
        sock = client._t._sock  # type: ignore[attr-defined]
        assert sock.gettimeout() is None, (
            f"socket should be back in blocking mode after hello; got timeout={sock.gettimeout()}"
        )
    finally:
        client.close()
