"""Stress: 50 concurrent Client.connect()s against one broker.

The Part-7.1 multi-client broker (commit `e93a54f`) replaced the
single-shot accept loop with thread-per-client + a Core/CapTable
mutex. This test pins that promise under load:

  * 50 clients connect concurrently. None blocks waiting for an
    earlier client; none errors.
  * Each connected client receives a positive `cap_id` from the
    hello handshake (Tier-0 #2 layer-1, CSPRNG-drawn).
  * Every client can do real work — each runs a `list()` after
    connect; all 50 see the same scope.
  * One client claims a device; all 50 OTHER clients see that
    lease via `list_leases` (cross-client visibility through the
    shared CapTable).
  * Tear-down: every client closes cleanly. The broker survives.
    A fresh client can connect after the storm.

Regression target: a single-threaded accept loop (the pre-7.1
state) would serialise — clients 2..50 would block until 1
finished its session, and the test would either deadlock or
exhaust the test timeout. A Core mutex held across an op would
deadlock under concurrent `claim` attempts.
"""

from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor, as_completed

import pytest

from mgf.hrb import ClaimMode, Client

pytestmark = [pytest.mark.stress, pytest.mark.timeout(60)]


_N_CLIENTS = 50


def test_many_concurrent_connects_all_succeed(broker: Client, sock_path: str) -> None:
    """All 50 connects succeed in parallel; each returns a positive
    cap_id; each can run a `list()` op. Confirms thread-per-client
    accept (Part 7.1) under sustained connect pressure."""

    def connect_and_probe() -> tuple[int, list[str]]:
        c = Client.connect(sock_path)
        try:
            urns = c.list("hw:/test/**")
            # Every connect MUST receive a positive cap_id from the
            # hello handshake (Tier-0 #2 layer-1) — `cap_id=0` is the
            # wire's "no cap" sentinel.
            cap_id = c._cap
            return cap_id, urns
        finally:
            c.close()

    results: list[tuple[int, list[str]]] = []
    with ThreadPoolExecutor(max_workers=_N_CLIENTS) as ex:
        futs = [ex.submit(connect_and_probe) for _ in range(_N_CLIENTS)]
        for f in as_completed(futs):
            results.append(f.result())

    assert len(results) == _N_CLIENTS

    for cap_id, _urns in results:
        assert cap_id > 0, f"cap_id should be positive, got {cap_id}"

    # Every client saw the same scope (the broker's view of the
    # fake bus is consistent across connections — G2 honest emptiness
    # plus shared-state coherence).
    canonical_urns = sorted(results[0][1])
    for _cap_id, urns in results:
        assert sorted(urns) == canonical_urns

    # Broker survives the storm: the fixture's client (`broker`)
    # was open the whole time and can still talk.
    assert broker.list("hw:/test/**") == canonical_urns


def test_one_lease_visible_to_all_other_clients(broker: Client, sock_path: str) -> None:
    """A held lease is visible across ALL concurrently-connected
    clients via `list_leases`. Confirms the shared CapTable serves
    every client a consistent view (the FEEDBACK SDK-03
    cross-process visibility promise) under N-client load."""
    n_observers = 20
    observers = [Client.connect(sock_path) for _ in range(n_observers)]
    try:
        # Holder takes one device.
        with broker.claim("hw:/test/dev0", ClaimMode.EXCLUSIVE) as held:
            # Every observer, queried concurrently, sees the same lease.
            def observe(c: Client) -> int | None:
                leases = c.list_leases("hw:/test/**")
                if len(leases) != 1:
                    return None
                return leases[0].lease_id

            with ThreadPoolExecutor(max_workers=n_observers) as ex:
                seen = list(ex.map(observe, observers))

            for lease_id in seen:
                assert lease_id == held.lease_id, (
                    f"observer saw lease_id={lease_id}, "
                    f"holder has {held.lease_id} — cross-client view diverged"
                )

        # After the holder's with-block exits, every observer sees zero
        # leases (G3 no ghost leases under multi-client load).
        for c in observers:
            assert c.list_leases("hw:/test/**") == []
    finally:
        for c in observers:
            c.close()


def test_broker_alive_after_connect_storm(broker: Client, sock_path: str) -> None:
    """After 50 connect/close cycles, a fresh client can still
    connect and do work. A torn-down client leaving the broker
    in a wedged state would show up here.

    The fixture's `broker` Client also stays alive — its socket
    isn't disturbed by other clients' lifecycle (Part 7.1: one
    thread per client, no shared connection state).
    """
    for _ in range(_N_CLIENTS):
        c = Client.connect(sock_path)
        c.close()

    # The fixture's client survived the storm.
    urns_before = broker.list("hw:/test/**")
    assert "hw:/test/dev0" in urns_before

    # A brand-new client connects + works after the churn.
    fresh = Client.connect(sock_path)
    try:
        assert fresh.list("hw:/test/**") == urns_before
        with fresh.claim("hw:/test/dev1", ClaimMode.EXCLUSIVE):
            leases = fresh.list_leases("hw:/test/**")
            assert len(leases) == 1
    finally:
        fresh.close()
