from __future__ import annotations

import itertools

import pytest

from ... import _core
from ...testing import assert_checkpoints, wait_all_tasks_blocked

pytestmark = pytest.mark.filterwarnings(
    "ignore:.*UnboundedQueue:trio.TrioDeprecationWarning"
)


async def test_UnboundedQueue_basic() -> None:
    q: _core.UnboundedQueue[str | int | None] = _core.UnboundedQueue()
    q.put_nowait("hi")
    assert await q.get_batch() == ["hi"]
    with pytest.raises(_core.WouldBlock):
        q.get_batch_nowait()
    q.put_nowait(1)
    q.put_nowait(2)
    q.put_nowait(3)
    assert q.get_batch_nowait() == [1, 2, 3]

    assert q.empty()
    assert q.qsize() == 0
    q.put_nowait(None)
    assert not q.empty()
    assert q.qsize() == 1

    stats = q.statistics()
    assert stats.qsize == 1
    assert stats.tasks_waiting == 0

    # smoke test
    repr(q)


async def test_UnboundedQueue_blocking() -> None:
    record = []
    q = _core.UnboundedQueue[int]()

    async def get_batch_consumer() -> None:
        while True:
            batch = await q.get_batch()
            assert batch
            record.append(batch)

    async def aiter_consumer() -> None:
        async for batch in q:
            assert batch
            record.append(batch)

    for consumer in (get_batch_consumer, aiter_consumer):
        record.clear()
        async with _core.open_nursery() as nursery:
            nursery.start_soon(consumer)
            await _core.wait_all_tasks_blocked()
            stats = q.statistics()
            assert stats.qsize == 0
            assert stats.tasks_waiting == 1
            q.put_nowait(10)
            q.put_nowait(11)
            await _core.wait_all_tasks_blocked()
            q.put_nowait(12)
            await _core.wait_all_tasks_blocked()
            assert record == [[10, 11], [12]]
            nursery.cancel_scope.cancel()


async def test_UnboundedQueue_fairness() -> None:
    q = _core.UnboundedQueue[int]()

    # If there's no-one else around, we can put stuff in and take it out
    # again, no problem
    q.put_nowait(1)
    q.put_nowait(2)
    assert q.get_batch_nowait() == [1, 2]

    result = None

    async def get_batch(q: _core.UnboundedQueue[int]) -> None:
        nonlocal result
        result = await q.get_batch()

    # But if someone else is waiting to read, then they get dibs
    async with _core.open_nursery() as nursery:
        nursery.start_soon(get_batch, q)
        await _core.wait_all_tasks_blocked()
        q.put_nowait(3)
        q.put_nowait(4)
        with pytest.raises(_core.WouldBlock):
            q.get_batch_nowait()
    assert result == [3, 4]

    # If two tasks are trying to read, they alternate
    record = []

    async def reader(name: str) -> None:
        while True:
            record.append((name, await q.get_batch()))

    async with _core.open_nursery() as nursery:
        nursery.start_soon(reader, "a")
        await _core.wait_all_tasks_blocked()
        nursery.start_soon(reader, "b")
        await _core.wait_all_tasks_blocked()

        for i in range(20):
            q.put_nowait(i)
            await _core.wait_all_tasks_blocked()

        nursery.cancel_scope.cancel()

    assert record == list(zip(itertools.cycle("ab"), [[i] for i in range(20)]))


async def test_UnboundedQueue_trivial_yields() -> None:
    q = _core.UnboundedQueue[None]()

    q.put_nowait(None)
    with assert_checkpoints():
        await q.get_batch()

    q.put_nowait(None)
    with assert_checkpoints():
        async for _ in q:  # pragma: no branch
            break


async def test_UnboundedQueue_no_spurious_wakeups() -> None:
    # If we have two tasks waiting, and put two items into the queue... then
    # only one task wakes up
    record = []

    async def getter(q: _core.UnboundedQueue[int], i: int) -> None:
        got = await q.get_batch()
        record.append((i, got))

    async with _core.open_nursery() as nursery:
        q = _core.UnboundedQueue[int]()
        nursery.start_soon(getter, q, 1)
        await wait_all_tasks_blocked()
        nursery.start_soon(getter, q, 2)
        await wait_all_tasks_blocked()

        for i in range(10):
            q.put_nowait(i)
        await wait_all_tasks_blocked()

        assert record == [(1, list(range(10)))]

        nursery.cancel_scope.cancel()
