"""R-T1-9B (survivability §3.7, Tier-1 #9, S5) — per-tenant
concurrent-connection ceiling.

The aggregate `MAX_ACTIVE_CLIENTS` bound alone lets ONE tenant open
every connection and starve everyone else. The per-uid cap
(`MAX_CONN_PER_UID`, keyed on the SO_PEERCRED uid) bounds how much of
the connection budget any single tenant holds. A tenant past its cap is
shed with a polite `TooBusy` line (the broker writes it INSTEAD of the
hello, then closes) — back-pressure, not a kernel connection-refused.

These tests open connections directly to the broker socket (all from the
test process → one uid) and assert the boundary lands at the cap.
"""

from __future__ import annotations

import json
import socket

import pytest

from tests.contract.conftest import _SOCK_PATH

pytestmark = pytest.mark.contract

# Must match `MAX_CONN_PER_UID` in hrb-code/src/server.zig.
MAX_CONN_PER_UID = 32


def _open_read_first_line(timeout: float = 2.0) -> tuple[socket.socket, dict]:
    """Open a fresh connection to the broker and read its first wire
    line — either the hello (admitted) or a TooBusy reject (over cap)."""
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.settimeout(timeout)
    s.connect(_SOCK_PATH)
    buf = b""
    while b"\n" not in buf:
        chunk = s.recv(4096)
        if not chunk:
            break
        buf += chunk
    line = buf.split(b"\n", 1)[0]
    return s, (json.loads(line) if line else {})


def test_per_tenant_connection_cap_sheds_with_too_busy(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """One uid can hold up to MAX_CONN_PER_UID live connections; the
    fixture already holds one, so MAX_CONN_PER_UID-1 fresh ones are
    admitted (get a hello) and the rest are shed with TooBusy."""
    _fixture_sock, _cap = raw_broker_with_cap  # holds 1 slot for our uid

    opened: list[socket.socket] = []
    hellos = 0
    too_busy = 0
    try:
        for _ in range(MAX_CONN_PER_UID + 5):
            s, first = _open_read_first_line()
            opened.append(s)
            if first.get("hello") == "mgf-hrb":
                hellos += 1
            elif first.get("err") == "TooBusy":
                too_busy += 1
            else:  # pragma: no cover - diagnostic
                pytest.fail(f"unexpected first line: {first!r}")

        # Fixture occupies one slot, so the cap admits MAX_CONN_PER_UID-1
        # fresh connections before shedding.
        assert hellos == MAX_CONN_PER_UID - 1, (
            f"expected {MAX_CONN_PER_UID - 1} admits (cap minus the fixture's "
            f"connection); got {hellos}"
        )
        assert too_busy >= 1, "the over-cap connections must be shed with TooBusy"
    finally:
        for s in opened:
            s.close()


def test_closing_a_connection_frees_a_slot(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """The cap tracks LIVE connections: closing one frees a slot, so a
    tenant that backs off can reconnect (transient back-pressure)."""
    _fixture_sock, _cap = raw_broker_with_cap

    opened: list[socket.socket] = []
    try:
        # Fill to the cap (fixture + MAX_CONN_PER_UID-1 fresh = cap).
        for _ in range(MAX_CONN_PER_UID - 1):
            s, first = _open_read_first_line()
            opened.append(s)
            assert first.get("hello") == "mgf-hrb"

        # At the cap: next is shed.
        s_over, over = _open_read_first_line()
        s_over.close()
        assert over.get("err") == "TooBusy"

        # Free a slot by closing one admitted connection. Closing makes
        # the broker's read return EOF -> handleClient exits -> the
        # per-uid slot is released. Poll briefly for the async release.
        opened.pop().close()
        admitted_again = False
        for _ in range(50):
            s_retry, retry = _open_read_first_line()
            opened.append(s_retry)
            if retry.get("hello") == "mgf-hrb":
                admitted_again = True
                break
            # still TooBusy -> the release hasn't landed yet; retry
        assert admitted_again, "a freed slot must admit a reconnect"
    finally:
        for s in opened:
            s.close()
