"""Tests for qremotex.execution.job_queue — JobQueue."""

import asyncio

import pytest

from qremotex.execution.job_queue import Job, JobQueue


def _make_job(request_id: str = "req-1", job_type: str = "script") -> Job:
    return Job(
        request_id=request_id,
        job_type=job_type,
        payload={"script_b64": "cHJpbnQoMSk="},
        timeout_sec=30,
    )


class TestJobQueue:

    @pytest.mark.asyncio
    async def test_enqueue_returns_position(self) -> None:
        executed: list[str] = []

        async def worker(job: Job) -> None:
            executed.append(job.request_id)

        q = JobQueue(concurrency=2, worker=worker)
        q.start()

        pos = await q.enqueue(_make_job("r1"))
        assert pos >= 1

        # Let dispatcher run
        await asyncio.sleep(0.1)
        await q.stop()

    @pytest.mark.asyncio
    async def test_fifo_order(self) -> None:
        executed: list[str] = []
        gate = asyncio.Event()

        async def worker(job: Job) -> None:
            await gate.wait()
            executed.append(job.request_id)

        q = JobQueue(concurrency=1, worker=worker)
        q.start()

        await q.enqueue(_make_job("r1"))
        await q.enqueue(_make_job("r2"))
        await q.enqueue(_make_job("r3"))

        # Let dispatcher pull jobs (concurrency=1, so only r1 starts)
        await asyncio.sleep(0.05)
        gate.set()
        await asyncio.sleep(0.2)
        await q.stop()

        # First should be r1
        assert executed[0] == "r1"

    @pytest.mark.asyncio
    async def test_concurrency_limit(self) -> None:
        active = 0
        max_active = 0
        gate = asyncio.Event()

        async def worker(job: Job) -> None:
            nonlocal active, max_active
            active += 1
            max_active = max(max_active, active)
            await gate.wait()
            active -= 1

        q = JobQueue(concurrency=2, worker=worker)
        q.start()

        for i in range(5):
            await q.enqueue(_make_job(f"r{i}"))

        await asyncio.sleep(0.1)
        assert max_active <= 2

        gate.set()
        await asyncio.sleep(0.3)
        await q.stop()

    @pytest.mark.asyncio
    async def test_cancel_queued_job(self) -> None:
        gate = asyncio.Event()
        executed: list[str] = []

        async def worker(job: Job) -> None:
            await gate.wait()
            executed.append(job.request_id)

        q = JobQueue(concurrency=1, worker=worker)
        q.start()

        # r1 will start immediately (blocks on gate), r2 stays queued
        await q.enqueue(_make_job("r1"))
        await asyncio.sleep(0.05)
        await q.enqueue(_make_job("r2"))

        cancelled = await q.cancel("r2")
        assert cancelled is True

        gate.set()
        await asyncio.sleep(0.2)
        await q.stop()
        assert "r2" not in executed

    @pytest.mark.asyncio
    async def test_cancel_nonexistent_returns_false(self) -> None:
        async def worker(job: Job) -> None:
            pass

        q = JobQueue(concurrency=1, worker=worker)
        q.start()
        result = await q.cancel("nonexistent")
        assert result is False
        await q.stop()

    @pytest.mark.asyncio
    async def test_queued_count(self) -> None:
        gate = asyncio.Event()

        async def worker(job: Job) -> None:
            await gate.wait()

        q = JobQueue(concurrency=1, worker=worker)
        q.start()

        await q.enqueue(_make_job("r1"))
        await asyncio.sleep(0.05)  # r1 dispatched
        await q.enqueue(_make_job("r2"))
        await q.enqueue(_make_job("r3"))

        assert q.queued_count >= 1  # r2, r3 queued (r1 running)

        gate.set()
        await asyncio.sleep(0.2)
        await q.stop()
