"""Comprehensive integration tests for fair task queue."""

import asyncio
from datetime import datetime, timedelta
from typing import Any

import pytest

from haolib.background.fair import FairTaskQueue
from haolib.background.fair.abstract import RateLimit, TaskStatus
from haolib.background.fair.exceptions import (
    FairQueueInvalidTaskError,
    FairQueueQueueFullError,
)
from haolib.background.fair.storages.memory import InMemoryFairQueueStorage
from tests.integration.background.fair.conftest import (
    create_task_invocation,
    group_key_extractor,
    mock_task_adapter,
)
from tests.integration.background.fair.constants import (
    BATCH_SIZE_MEDIUM,
    BATCH_SIZE_SMALL,
    TASK_COUNT_VERY_LARGE,
)


class TestFairTaskQueueBasicOperations:
    """Test basic queue operations."""

    @pytest.mark.asyncio
    async def test_enqueue_and_dequeue_single_task(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test enqueueing and dequeuing a single task."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            task = create_task_invocation(kwargs={"user_id": "user_1"})
            task_id = await queue.enqueue(task, group_id="user_1")

            assert task_id is not None
            assert await queue.get_task_status(task_id, "user_1") == TaskStatus.PENDING

    @pytest.mark.asyncio
    async def test_enqueue_with_priority(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test enqueueing tasks with different priorities."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            task1 = create_task_invocation(kwargs={"user_id": "user_1"})
            task2 = create_task_invocation(kwargs={"user_id": "user_1"})

            await queue.enqueue(task1, group_id="user_1", priority=1)
            task_id_high = await queue.enqueue(task2, group_id="user_1", priority=10)

            # Higher priority task should be dequeued first
            dequeued = await in_memory_storage.dequeue_task("user_1")
            assert dequeued is not None
            assert dequeued.task_id == task_id_high

    @pytest.mark.asyncio
    async def test_enqueue_batch_simple(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test batch enqueue with simple list format."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            tasks = [
                create_task_invocation(kwargs={"user_id": "user_1"}),
                create_task_invocation(kwargs={"user_id": "user_1"}),
                create_task_invocation(kwargs={"user_id": "user_1"}),
            ]

            task_ids = await queue.enqueue_batch(tasks, group_id="user_1")

            assert len(task_ids) == BATCH_SIZE_SMALL
            assert all(tid is not None for tid in task_ids)
            assert await queue.get_group_queue_length("user_1") == BATCH_SIZE_SMALL

    @pytest.mark.asyncio
    async def test_wait_for_result_auto_derive_group_id(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test wait_for_result auto-derives group_id."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            task = create_task_invocation(kwargs={"user_id": "user_1"})
            task_id = await queue.enqueue(task, group_id="user_1")

            # Should work without providing group_id
            # Note: This will timeout in real scenario, but tests the auto-derive logic
            with pytest.raises((TimeoutError, RuntimeError)):
                await queue.wait_for_result(task_id, timeout_seconds=0.1)

    @pytest.mark.asyncio
    async def test_queue_size_limits(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test queue size limits."""
        queue = FairTaskQueue(
            storage=in_memory_storage,
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,
            max_queue_size_per_group=2,
        )

        async with queue:
            task1 = create_task_invocation(kwargs={"user_id": "user_1"})
            task2 = create_task_invocation(kwargs={"user_id": "user_1"})
            task3 = create_task_invocation(kwargs={"user_id": "user_1"})

            await queue.enqueue(task1, group_id="user_1")
            await queue.enqueue(task2, group_id="user_1")

            # Third task should fail
            with pytest.raises(FairQueueQueueFullError):
                await queue.enqueue(task3, group_id="user_1")

    @pytest.mark.asyncio
    async def test_idempotency(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test idempotency key handling."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            task = create_task_invocation(kwargs={"user_id": "user_1"})
            idempotency_key = "unique_key_123"

            task_id1 = await queue.enqueue(task, group_id="user_1", idempotency_key=idempotency_key)
            task_id2 = await queue.enqueue(task, group_id="user_1", idempotency_key=idempotency_key)

            # Should return same task ID
            assert task_id1 == task_id2

    @pytest.mark.asyncio
    async def test_rate_limiting(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test rate limiting per group."""
        queue = FairTaskQueue(
            storage=in_memory_storage,
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,
            default_rate_limit=RateLimit(max_tasks=2, per_time=timedelta(seconds=1)),
        )

        async with queue:
            task1 = create_task_invocation(kwargs={"user_id": "user_1"})
            task2 = create_task_invocation(kwargs={"user_id": "user_1"})
            task3 = create_task_invocation(kwargs={"user_id": "user_1"})

            # First two should succeed
            await queue.enqueue(task1, group_id="user_1")
            await queue.enqueue(task2, group_id="user_1")

            # Third should be rate limited (but enqueue still succeeds, rate limit applies at dequeue)
            await queue.enqueue(task3, group_id="user_1")

    @pytest.mark.asyncio
    async def test_task_dependencies(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test task dependency handling."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            task1 = create_task_invocation(kwargs={"user_id": "user_1"})
            task2 = create_task_invocation(kwargs={"user_id": "user_1"})

            task_id1 = await queue.enqueue(task1, group_id="user_1")
            await queue.enqueue(task2, group_id="user_1", depends_on=[task_id1])

            # Task2 should not be processed until task1 completes
            dequeued = await in_memory_storage.dequeue_task("user_1")
            assert dequeued is not None
            assert dequeued.task_id == task_id1  # Task1 should be dequeued first

    @pytest.mark.asyncio
    async def test_circular_dependency_detection(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test circular dependency detection."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            # Test: Create a dependency chain and verify it works
            task1 = create_task_invocation(kwargs={"user_id": "user_1"})
            task2 = create_task_invocation(kwargs={"user_id": "user_1"})

            task_id1 = await queue.enqueue(task1, group_id="user_1")
            task_id2 = await queue.enqueue(task2, group_id="user_1", depends_on=[task_id1])

            # Verify dependencies are set correctly
            task_info1 = await queue.get_task_info(task_id1, "user_1")
            task_info2 = await queue.get_task_info(task_id2, "user_1")
            assert task_info1 is not None
            assert task_info2 is not None
            # Verify task2 depends on task1 by checking the task directly
            task2_data = await in_memory_storage.get_task(task_id2, "user_1")
            assert task2_data is not None
            assert task_id1 in task2_data.depends_on


class TestFairTaskQueueErrorHandling:
    """Test error handling and recovery."""

    @pytest.mark.asyncio
    async def test_storage_error_handling(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test handling of storage errors."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            # Create invalid task (None task)
            with pytest.raises(FairQueueInvalidTaskError):
                await queue.enqueue(None, group_id="user_1")  # type: ignore[arg-type]

    @pytest.mark.asyncio
    async def test_invalid_group_id(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test validation of group IDs."""
        queue = FairTaskQueue(
            storage=in_memory_storage,
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,
            max_group_id_length=10,
        )

        async with queue:
            task = create_task_invocation(kwargs={"user_id": "user_1"})

            # Too long group ID
            with pytest.raises(FairQueueInvalidTaskError):
                await queue.enqueue(task, group_id="a" * 20)

    @pytest.mark.asyncio
    async def test_invalid_priority(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test validation of priority values."""
        queue = FairTaskQueue(
            storage=in_memory_storage,
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,
            min_priority=-10,
            max_priority=10,
        )

        async with queue:
            task = create_task_invocation(kwargs={"user_id": "user_1"})

            # Priority out of range
            with pytest.raises(FairQueueInvalidTaskError):
                await queue.enqueue(task, group_id="user_1", priority=100)


class TestFairTaskQueueMultiInstance:
    """Test multi-instance scenarios."""

    @pytest.mark.asyncio
    async def test_concurrent_enqueue(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test concurrent enqueue operations."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            tasks = [create_task_invocation(kwargs={"user_id": f"user_{i}"}) for i in range(10)]

            # Enqueue concurrently
            task_ids = await asyncio.gather(
                *[queue.enqueue(task, group_id=f"user_{i}") for i, task in enumerate(tasks)]
            )

            assert len(task_ids) == TASK_COUNT_VERY_LARGE
            assert len(set(task_ids)) == TASK_COUNT_VERY_LARGE  # All unique

    @pytest.mark.asyncio
    async def test_fairness_across_groups(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test fairness algorithm distributes tasks across groups."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            # Enqueue tasks for multiple groups
            for group_id in ["group_1", "group_2", "group_3"]:
                for _ in range(5):
                    task = create_task_invocation(kwargs={"user_id": group_id})
                    await queue.enqueue(task, group_id=group_id)

            # Verify all groups have tasks
            assert await queue.get_group_queue_length("group_1") == BATCH_SIZE_MEDIUM
            assert await queue.get_group_queue_length("group_2") == BATCH_SIZE_MEDIUM
            assert await queue.get_group_queue_length("group_3") == BATCH_SIZE_MEDIUM


class TestFairTaskQueueRecovery:
    """Test recovery and stuck task handling."""

    @pytest.mark.asyncio
    async def test_recover_stuck_tasks(
        self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any
    ) -> None:
        """Test recovery of stuck tasks."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            task = create_task_invocation(kwargs={"user_id": "user_1"})
            task_id = await queue.enqueue(task, group_id="user_1")

            # Manually mark as processing (simulating stuck task)
            await in_memory_storage.update_task_status(task_id, "user_1", TaskStatus.PROCESSING)

            # Recover stuck tasks
            recovered = await queue.recover_stuck_tasks(max_processing_time=timedelta(seconds=1))

            # Should recover the stuck task
            assert recovered >= 0

    @pytest.mark.asyncio
    async def test_auto_recovery(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test automatic recovery of stuck tasks."""
        queue = FairTaskQueue(
            storage=in_memory_storage,
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,
            enable_auto_recovery=True,
            auto_recovery_interval=timedelta(seconds=1),
            auto_recovery_max_processing_time=timedelta(seconds=1),
        )

        async with queue:
            task = create_task_invocation(kwargs={"user_id": "user_1"})
            task_id = await queue.enqueue(task, group_id="user_1")

            # Manually mark as processing
            await in_memory_storage.update_task_status(task_id, "user_1", TaskStatus.PROCESSING)

            # Wait for auto-recovery
            await asyncio.sleep(2)

            # Task should be recovered
            status = await queue.get_task_status(task_id, "user_1")
            assert status in (TaskStatus.PENDING, TaskStatus.PROCESSING)


class TestFairTaskQueueHealth:
    """Test health checks and monitoring."""

    @pytest.mark.asyncio
    async def test_health_check(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test health check functionality."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            healthy = await queue.health_check()
            assert healthy is True

            health_status = await queue.get_health_status()
            assert health_status.healthy is True
            assert health_status.component == "fair_task_queue"

    @pytest.mark.asyncio
    async def test_queue_metrics(self, in_memory_storage: InMemoryFairQueueStorage, mock_task_adapter: Any) -> None:
        """Test queue metrics collection."""
        queue = FairTaskQueue(
            storage=in_memory_storage,  # type: ignore[arg-type]
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,  # type: ignore[arg-type]
        )

        async with queue:
            # Enqueue some tasks
            for i in range(5):
                task = create_task_invocation(kwargs={"user_id": f"user_{i}"})
                await queue.enqueue(task, group_id=f"user_{i}")

            metrics = await queue.get_queue_metrics()

            assert metrics.total_groups == BATCH_SIZE_MEDIUM
            assert metrics.total_queue_length == BATCH_SIZE_MEDIUM
            assert metrics.is_paused is False
