"""The citation-verification view must not fan out without a bound.

`build_view` used to spawn one retrieval task per legal argument with a bare
`asyncio.gather`. Each task opens a Voyage embed call, and past ~8 in flight
Voyage throttles — so the unbounded version was *slower* than a bounded one
(32 arguments: 30.4s unbounded vs 22.1s at 8), and a 69-argument case blew past
the 30s proxy timeout and returned 500 while overlapping reloads piled
contention into Postgres deadlocks.

These tests pin the two properties that fix gave us: the fan-out is bounded,
and a timeout yields the results that DID land instead of nothing.
"""

import asyncio

import pytest

from legal_mcp.services import case_citation_verification as ccv


def test_concurrency_bound_is_set_and_modest():
    assert 1 <= ccv._MAX_CONCURRENT_LOOKUPS <= 16, (
        "the bound exists to stay under Voyage's throttle point — a large value "
        "reintroduces the stall this was added to fix"
    )


def test_retrieval_budget_leaves_room_under_the_proxy_timeout():
    """The proxy gives up at 30s; we must cut ourselves off before that."""
    assert 0 < ccv._RETRIEVAL_BUDGET_S < 30


@pytest.mark.asyncio
async def test_semaphore_actually_caps_in_flight_work():
    """A semaphore of N never lets N+1 coroutines run the body at once."""
    limit = ccv._MAX_CONCURRENT_LOOKUPS
    sem = asyncio.Semaphore(limit)
    in_flight = 0
    peak = 0

    async def worker():
        nonlocal in_flight, peak
        async with sem:
            in_flight += 1
            peak = max(peak, in_flight)
            await asyncio.sleep(0.01)
            in_flight -= 1

    await asyncio.gather(*(worker() for _ in range(limit * 4)))
    assert peak <= limit


@pytest.mark.asyncio
async def test_timeout_harvests_finished_work_instead_of_discarding_it():
    """The regression: wait_for(gather(...)) cancels everything on timeout, so
    one slow lookup threw away every result that had already succeeded. The
    harvest pattern must keep them."""

    async def quick(i):
        await asyncio.sleep(0.01)
        return i

    async def never():
        await asyncio.sleep(30)
        return "unreachable"

    tasks = [asyncio.ensure_future(quick(i)) for i in range(5)]
    tasks.append(asyncio.ensure_future(never()))

    done, pending = await asyncio.wait(tasks, timeout=0.3)
    for t in pending:
        t.cancel()
    await asyncio.gather(*pending, return_exceptions=True)

    harvested = [
        t.result() if (t in done and not t.cancelled() and t.exception() is None) else None
        for t in tasks
    ]
    assert harvested[:5] == [0, 1, 2, 3, 4], "finished work must survive the timeout"
    assert harvested[5] is None, "the unfinished one is empty, not fabricated"
    assert len(pending) == 1
