"""Tests for bounded application parallelism — Task 42."""

from __future__ import annotations

import asyncio

import pytest

from services.intelligence_pipeline_v3.orchestrator.parallelism import (
    AdjudicatorSemaphore,
    AsyncWorkerPool,
    DocumentPriority,
    LoadSheddingAction,
    MicroBatcher,
    WorkerPoolConfig,
)

# ---------------------------------------------------------------------------
# Worker Pool Tests
# ---------------------------------------------------------------------------


class TestAsyncWorkerPool:
    """Task 42.1: Configurable async workers replacing sequential loop."""

    @pytest.mark.asyncio
    async def test_submit_processes_work(self):
        pool = AsyncWorkerPool(WorkerPoolConfig(max_workers=2))
        results = []

        async def work(value: int):
            results.append(value)

        result = await pool.submit(work, 42)
        assert result is None  # No shedding
        await asyncio.sleep(0.05)
        assert 42 in results

    @pytest.mark.asyncio
    async def test_available_slots(self):
        pool = AsyncWorkerPool(WorkerPoolConfig(max_workers=4))
        assert pool.available_slots == 4

    @pytest.mark.asyncio
    async def test_load_shedding_rejects_low_priority(self):
        config = WorkerPoolConfig(
            max_workers=1, queue_max_depth=1, shed_threshold=0.5
        )
        pool = AsyncWorkerPool(config)
        pool._stats.queued_items = 1  # Simulate full queue
        assert pool.should_shed_load()

        async def noop():
            pass

        result = await pool.submit(
            noop, priority=DocumentPriority.LOW
        )
        assert result == LoadSheddingAction.REJECT

    @pytest.mark.asyncio
    async def test_safety_critical_never_shed(self):
        config = WorkerPoolConfig(
            max_workers=1, queue_max_depth=1, shed_threshold=0.5
        )
        pool = AsyncWorkerPool(config)
        pool._stats.queued_items = 1  # Simulate full queue

        async def noop():
            pass

        result = await pool.submit(
            noop, priority=DocumentPriority.SAFETY_CRITICAL
        )
        # Safety-critical is never rejected
        assert result is None

    @pytest.mark.asyncio
    async def test_stats_track_processed(self):
        pool = AsyncWorkerPool(WorkerPoolConfig(max_workers=2))

        async def work():
            pass

        await pool.submit(work)
        await asyncio.sleep(0.05)
        assert pool.stats.processed_total >= 1

    @pytest.mark.asyncio
    async def test_shutdown(self):
        pool = AsyncWorkerPool()
        await pool.start()
        assert pool.is_running
        await pool.shutdown(timeout=1.0)
        assert not pool.is_running


# ---------------------------------------------------------------------------
# Adjudicator Semaphore Tests
# ---------------------------------------------------------------------------


class TestAdjudicatorSemaphore:
    """Task 42.3: GPU-safe concurrency semaphore."""

    @pytest.mark.asyncio
    async def test_acquire_and_release(self):
        sem = AdjudicatorSemaphore(max_concurrent=2)
        assert await sem.acquire()
        assert sem.active_count == 1
        sem.release()
        assert sem.active_count == 0

    @pytest.mark.asyncio
    async def test_backpressure_when_queue_full(self):
        sem = AdjudicatorSemaphore(max_concurrent=2, max_queued=0)
        # Queue is immediately "full"
        result = await sem.acquire()
        assert result is False

    @pytest.mark.asyncio
    async def test_utilization(self):
        sem = AdjudicatorSemaphore(max_concurrent=4)
        await sem.acquire()
        await sem.acquire()
        assert sem.utilization == 0.5

    @pytest.mark.asyncio
    async def test_is_backpressured(self):
        sem = AdjudicatorSemaphore(max_concurrent=2, max_queued=1)
        sem._queued = 1
        assert sem.is_backpressured


# ---------------------------------------------------------------------------
# Micro-Batcher Tests
# ---------------------------------------------------------------------------


class TestMicroBatcher:
    """Task 42.2: Specialist micro-batching."""

    def test_batch_fills_at_size(self):
        batcher = MicroBatcher(batch_size=3)
        assert batcher.add("a") is None
        assert batcher.add("b") is None
        batch = batcher.add("c")
        assert batch == ["a", "b", "c"]
        assert batcher.is_empty

    def test_flush_returns_partial(self):
        batcher = MicroBatcher(batch_size=10)
        batcher.add("x")
        batcher.add("y")
        batch = batcher.flush()
        assert batch == ["x", "y"]
        assert batcher.is_empty

    def test_pending_count(self):
        batcher = MicroBatcher(batch_size=5)
        batcher.add(1)
        batcher.add(2)
        assert batcher.pending_count == 2

    def test_total_batches_tracked(self):
        batcher = MicroBatcher(batch_size=2)
        batcher.add(1)
        batcher.add(2)  # First batch
        batcher.add(3)
        batcher.add(4)  # Second batch
        assert batcher.total_batches == 2
