"""Chaos engineering tests for fair task queue.

These tests simulate real-world failure scenarios to ensure
the queue handles them gracefully.
"""

import asyncio
from datetime import timedelta
from typing import Any

import pytest

from haolib.background.fair import FairTaskQueue
from haolib.background.fair.abstract import EnqueueOptions, GroupKeyExtractor
from haolib.background.fair.exceptions import FairQueueQueueFullError, FairQueueStorageError
from haolib.background.fair.storages.memory import InMemoryFairQueueStorage
from tests.integration.background.fair.conftest import (
    create_task_invocation,
)


class TestChaosScenarios:
    """Chaos engineering tests for failure scenarios."""

    @pytest.mark.asyncio
    async def test_storage_connection_loss_during_enqueue(
        self,
        in_memory_storage: InMemoryFairQueueStorage,
        mock_task_adapter: Any,
        group_key_extractor: GroupKeyExtractor,
    ) -> None:
        """Test handling of storage connection loss during enqueue."""
        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:
            # Simulate storage failure by patching
            original_enqueue = in_memory_storage.enqueue_task

            async def failing_enqueue(task: Any) -> None:
                """Simulate storage failure."""
                raise FairQueueStorageError(
                    detail="Connection lost",
                    operation="enqueue_task",
                    task_id=task.task_id,
                )

            # Use setattr to avoid type checker issues
            in_memory_storage.enqueue_task = failing_enqueue  # type: ignore[method-assign]

            task = create_task_invocation(kwargs={"user_id": "user_1"})
            with pytest.raises(FairQueueStorageError) as exc_info:
                await queue.enqueue(task, group_id="user_1")

            # Verify error message is actionable
            assert "Possible causes" in str(exc_info.value.detail) or "Recovery steps" in str(exc_info.value.detail)

            # Restore original method
            in_memory_storage.enqueue_task = original_enqueue  # type: ignore[method-assign]

    @pytest.mark.asyncio
    async def test_concurrent_enqueue_race_condition(
        self,
        in_memory_storage: InMemoryFairQueueStorage,
        mock_task_adapter: Any,
        group_key_extractor: GroupKeyExtractor,
    ) -> None:
        """Test handling of concurrent enqueue operations."""
        total_tasks = 100
        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 many tasks concurrently
            tasks = [create_task_invocation(kwargs={"user_id": f"user_{i % 10}"}) for i in range(total_tasks)]
            task_ids = await asyncio.gather(
                *[queue.enqueue(task, group_id=f"user_{i % 10}") for i, task in enumerate(tasks)],
                return_exceptions=True,
            )

            # All should succeed (no race conditions)
            assert all(not isinstance(tid, Exception) for tid in task_ids)
            assert len(set(task_ids)) == total_tasks  # All unique

    @pytest.mark.asyncio
    async def test_queue_full_with_backpressure(
        self,
        in_memory_storage: InMemoryFairQueueStorage,
        mock_task_adapter: Any,
        group_key_extractor: GroupKeyExtractor,
    ) -> None:
        """Test backpressure handling when queue is full."""
        queue = FairTaskQueue(
            storage=in_memory_storage,
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,
            max_queue_size_per_group=5,
        )

        async with queue:
            # Fill queue
            for _ in range(5):
                task = create_task_invocation(kwargs={"user_id": "user_1"})
                await queue.enqueue(task, group_id="user_1")

            # Stop scheduler to prevent tasks from being processed
            # This ensures the queue stays full for the backpressure test
            await queue.stop_scheduler(graceful=False)

            # Try to enqueue with backpressure (should wait and timeout since queue is full)
            task = create_task_invocation(kwargs={"user_id": "user_1"})
            options = EnqueueOptions(
                group_id="user_1",
                backpressure=True,
                backpressure_max_wait=timedelta(seconds=0.5),  # Short timeout for test
                backpressure_retry_interval=timedelta(seconds=0.1),  # Explicit retry interval
            )

            # Should timeout since queue is full and no tasks are being processed
            with pytest.raises(FairQueueQueueFullError):
                await queue.enqueue(task, options=options)

    @pytest.mark.asyncio
    async def test_scheduler_crash_recovery(
        self,
        in_memory_storage: InMemoryFairQueueStorage,
        mock_task_adapter: Any,
        group_key_extractor: GroupKeyExtractor,
    ) -> None:
        """Test recovery from scheduler crash."""
        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 _ in range(5):
                task = create_task_invocation(kwargs={"user_id": "user_1"})
                await queue.enqueue(task, group_id="user_1")

            # Simulate scheduler crash
            await queue.stop_scheduler(graceful=False)

            # Restart scheduler
            await queue.start_scheduler()

            # Tasks should still be processable
            await asyncio.sleep(0.5)
            metrics = await queue.get_queue_metrics()
            # Tasks may have been processed or still pending
            assert metrics.total_queue_length >= 0

    @pytest.mark.asyncio
    async def test_network_partition_simulation(
        self,
        in_memory_storage: InMemoryFairQueueStorage,
        mock_task_adapter: Any,
        group_key_extractor: GroupKeyExtractor,
    ) -> None:
        """Test handling of network partition (storage unavailable)."""
        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:
            # Simulate network partition by making storage unavailable

            async def failing_get(group_id: str) -> int:
                """Simulate network failure."""
                raise FairQueueStorageError(
                    detail="Network partition - storage unavailable",
                    operation="get_group_queue_length",
                )

            # Use monkeypatch to replace the method
            monkeypatch = pytest.MonkeyPatch()
            monkeypatch.setattr(in_memory_storage, "get_group_queue_length", failing_get)

            # Health check should detect issue (may or may not be healthy)
            # But should not crash
            await queue.get_health_status()

            # Restore
            monkeypatch.undo()

    @pytest.mark.asyncio
    async def test_memory_pressure(
        self,
        in_memory_storage: InMemoryFairQueueStorage,
        mock_task_adapter: Any,
        group_key_extractor: GroupKeyExtractor,
    ) -> None:
        """Test handling under memory pressure (many tasks)."""
        queue = FairTaskQueue(
            storage=in_memory_storage,
            task_adapter=mock_task_adapter,
            group_key_extractor=group_key_extractor,
            max_pending_results=100,
            max_result_events=100,
        )

        async with queue:
            # Enqueue many tasks
            tasks = [create_task_invocation(kwargs={"user_id": f"user_{i % 10}"}) for i in range(1000)]
            task_ids = await asyncio.gather(
                *[queue.enqueue(task, group_id=f"user_{i % 10}") for i, task in enumerate(tasks)],
                return_exceptions=True,
            )

            # Should handle gracefully (may reject some if limits hit)
            success_count = sum(1 for tid in task_ids if not isinstance(tid, Exception))
            assert success_count > 0

            # Memory should be bounded
            scheduler = queue.scheduler
            assert scheduler.pending_results_count <= queue.max_pending_results
            assert scheduler.result_events_count <= queue.max_result_events

    @pytest.mark.asyncio
    async def test_cascading_failures(
        self,
        in_memory_storage: InMemoryFairQueueStorage,
        mock_task_adapter: Any,
        group_key_extractor: GroupKeyExtractor,
    ) -> None:
        """Test handling of cascading failures."""
        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:
            # Make adapter fail
            original_dispatch = mock_task_adapter.dispatch

            async def failing_dispatch(task: Any) -> None:
                """Simulate adapter failure."""
                raise RuntimeError("Adapter failure")

            mock_task_adapter.dispatch = failing_dispatch

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

            # Wait for processing attempt
            await asyncio.sleep(1)

            # Task should be retried or moved to DLQ
            task_info = await queue.get_task_info(task_id, "user_1")
            if task_info and task_info.status:
                assert task_info.status in ("pending", "failed", "processing")

            # Restore
            mock_task_adapter.dispatch = original_dispatch
