"""SeatPool: `/v1/ask` takes any free seat; an agentic conversation stays pinned to
the seat it opened on; one seat's cap fails the whole pool fast.

Nothing here spends a real ChatGPT turn or opens a real browser.
"""

import json
import threading

import pytest

import chat
import solwebd
from fake_browser import block, pool_for, seat_for

SYSTEM = {"role": "system", "content": "be terse"}
ASK = {"role": "user", "content": "hi"}
DONE = block(json.dumps({"tool": "final", "text": "ok"}))


def request(messages, model="sol-web-medium"):
    return {"model": model, "messages": messages, "tools": []}


@pytest.fixture(autouse=True)
def instant_waits(monkeypatch):
    monkeypatch.setattr(chat.time, "sleep", lambda *_: None)


def test_ask_uses_whichever_seat_is_free_not_the_one_thats_busy():
    pool, marionettes = pool_for([[{"blocks": [], "text": "a"}], [{"blocks": [], "text": "b"}]])
    engine = solwebd.Engine(pool)
    engine.pool.locks[0].acquire()  # seat 0 held by something else
    done = threading.Event()
    threading.Thread(target=lambda: (engine.ask({"prompt": "hi", "stamp": "s"}), done.set()),
                     daemon=True).start()
    assert done.wait(2), "ask should have used the other free seat instead of blocking"
    assert marionettes[1].typed, "seat 1 should have served the request"
    assert not marionettes[0].typed, "seat 0 was held, it must not have been touched"
    engine.pool.locks[0].release()


def test_agentic_turns_wait_for_their_own_pinned_seat_even_when_another_is_free():
    pool, marionettes = pool_for([[DONE, DONE], []])
    engine = solwebd.Engine(pool)

    engine.complete(request([SYSTEM, ASK]))
    assert marionettes[0].typed and not marionettes[1].typed, \
        "the first free seat (0) should have opened the conversation"

    engine.pool.locks[0].acquire()  # simulate seat 0 mid-turn elsewhere
    done = threading.Event()
    follow_up = request([SYSTEM, ASK, {"role": "user", "content": "again"}])
    threading.Thread(target=lambda: (engine.complete(follow_up), done.set()),
                     daemon=True).start()
    assert not done.wait(0.3), "a pinned turn must wait for its own seat, not grab seat 1"
    engine.pool.locks[0].release()
    assert done.wait(2)
    assert not marionettes[1].typed, "seat 1 must stay untouched by a pinned conversation"


def test_simultaneous_turns_of_one_conversation_never_split_across_seats():
    # Two same-key requests arriving together: without the engine's key lock both
    # can find no registered conversation, take two different seats, and one turn
    # lands on a browser showing another conversation.
    pool, marionettes = pool_for([[DONE, DONE], [DONE, DONE]])
    engine = solwebd.Engine(pool)
    barrier = threading.Barrier(2)
    errors: list[Exception] = []

    def go():
        barrier.wait()
        try:
            engine.complete(request([SYSTEM, ASK]))
        except Exception as exc:  # noqa: BLE001 — surfaced via the assertion below
            errors.append(exc)

    threads = [threading.Thread(target=go) for _ in range(2)]
    for t in threads:
        t.start()
    for t in threads:
        t.join(5)
    assert not errors, f"both turns must serve cleanly, got {errors}"
    assert marionettes[0].typed and not marionettes[1].typed, \
        "one conversation must stay on one seat, whichever request wins"
    assert len(engine.registry) == 1


def test_a_capped_seat_fails_the_whole_pool_fast_without_touching_a_free_seat():
    pool, marionettes = pool_for([[], []])
    marionettes[0].cap = {"text": "You've hit your usage limit. Try again at 3:45 PM."}
    engine = solwebd.Engine(pool)

    with pytest.raises(solwebd.Capped):
        engine.ask({"prompt": "hi", "stamp": "s1"})  # lands on seat 0, discovers the cap
    assert engine.pool.capped() is not None

    with pytest.raises(solwebd.Capped):
        engine.ask({"prompt": "hi", "stamp": "s2"})  # must fail fast pool-wide
    assert marionettes[1].navigations == [], \
        "a fast-fail must never spend a turn on the other, uncapped seat"


def test_seats_default_to_one_and_the_flag_builds_a_bigger_pool():
    pool_default = solwebd.SeatPool(session_factory=lambda slot: object())
    assert len(pool_default.seats) == 1
    pool_two = solwebd.SeatPool(max_seats=2, session_factory=lambda slot: object())
    assert solwebd.Engine(pool_two).health()["seats"] == 2


def test_main_rejects_a_seats_count_outside_the_slot_range(monkeypatch, capsys):
    monkeypatch.setattr("sys.argv", ["solwebd", "--seats", "0"])
    with pytest.raises(SystemExit) as caught:
        solwebd.main()
    assert caught.value.code == 2
    assert "--seats" in capsys.readouterr().err


def test_serve_seat_argument_is_rejected_alongside_multiple_seats():
    seat, _ = seat_for([])
    with pytest.raises(ValueError):
        solwebd.serve(seat=seat, seats=2)


def test_capped_state_expires_after_the_cooldown(monkeypatch):
    pool, _ = pool_for([[]])
    now = [0.0]
    monkeypatch.setattr(solwebd.time, "monotonic", lambda: now[0])
    pool.mark_capped({"text": "capped"})
    assert pool.capped() is not None
    now[0] = solwebd.CAP_COOLDOWN_SECONDS + 1
    assert pool.capped() is None
