"""Pytest configuration and fixtures for OCLAWMA tests."""

from __future__ import annotations

import asyncio
import tempfile
from collections.abc import AsyncGenerator, Generator
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock

import pytest
import pytest_asyncio

from oclawma.cache import CacheConfig, ResultCache
from oclawma.encryption import EncryptionManager
from oclawma.queue.dlq import DeadLetterQueue
from oclawma.queue.models import Job, JobPriority, JobStatus
from oclawma.queue.queue import JobQueue
from oclawma.queue.store import JobStore
from oclawma.ratelimit import RateLimitConfig, RateLimiter

# =============================================================================
# Async Event Loop Fixture
# =============================================================================


@pytest.fixture(scope="session")
def event_loop():
    """Create an instance of the default event loop for the test session."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()


@pytest_asyncio.fixture
async def async_event_loop() -> AsyncGenerator[asyncio.AbstractEventLoop, None]:
    """Provide an async event loop for async tests."""
    loop = asyncio.get_running_loop()
    yield loop


# =============================================================================
# Database Fixtures
# =============================================================================


@pytest.fixture
def temp_db_path() -> Generator[Path, None, None]:
    """Create a temporary database file path."""
    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
        path = Path(f.name)
    yield path
    # Cleanup after test
    if path.exists():
        path.unlink()


@pytest.fixture
def in_memory_store() -> Generator[JobStore, None, None]:
    """Provide an in-memory job store for testing."""
    store = JobStore(":memory:")
    yield store
    store.close()


@pytest.fixture
def temp_store(temp_db_path: Path) -> Generator[JobStore, None, None]:
    """Provide a job store backed by a temporary file."""
    store = JobStore(temp_db_path)
    yield store
    store.close()


@pytest.fixture
def in_memory_queue() -> Generator[JobQueue, None, None]:
    """Provide an in-memory job queue for testing."""
    queue = JobQueue(":memory:", base_delay_seconds=0)
    yield queue
    queue.close()


@pytest.fixture
def temp_queue(temp_db_path: Path) -> Generator[JobQueue, None, None]:
    """Provide a job queue backed by a temporary file."""
    queue = JobQueue(temp_db_path)
    yield queue
    queue.close()


# =============================================================================
# Mock Job Fixtures
# =============================================================================


@pytest.fixture
def sample_payload() -> dict[str, Any]:
    """Provide a sample job payload."""
    return {
        "task": "test_task",
        "data": {"key": "value", "number": 42},
        "metadata": {"source": "test", "version": "1.0"},
    }


@pytest.fixture
def mock_job(sample_payload: dict[str, Any]) -> Job:
    """Create a basic mock job."""
    return Job(
        id=1,
        payload=sample_payload,
        status=JobStatus.PENDING,
        priority=JobPriority.NORMAL,
    )


@pytest.fixture
def mock_pending_job(sample_payload: dict[str, Any]) -> Job:
    """Create a pending mock job."""
    return Job(
        id=1,
        payload=sample_payload,
        status=JobStatus.PENDING,
        priority=JobPriority.NORMAL,
    )


@pytest.fixture
def mock_running_job(sample_payload: dict[str, Any]) -> Job:
    """Create a running mock job."""
    return Job(
        id=2,
        payload=sample_payload,
        status=JobStatus.RUNNING,
        priority=JobPriority.HIGH,
    )


@pytest.fixture
def mock_completed_job(sample_payload: dict[str, Any]) -> Job:
    """Create a completed mock job."""
    return Job(
        id=3,
        payload=sample_payload,
        status=JobStatus.COMPLETED,
        priority=JobPriority.LOW,
    )


@pytest.fixture
def mock_failed_job(sample_payload: dict[str, Any]) -> Job:
    """Create a failed mock job."""
    return Job(
        id=4,
        payload=sample_payload,
        status=JobStatus.FAILED,
        priority=JobPriority.NORMAL,
        retry_count=1,
        max_retries=3,
        error="Test failure",
    )


@pytest.fixture
def mock_critical_job(sample_payload: dict[str, Any]) -> Job:
    """Create a critical priority mock job."""
    return Job(
        id=5,
        payload=sample_payload,
        status=JobStatus.PENDING,
        priority=JobPriority.CRITICAL,
    )


@pytest.fixture
def mock_jobs_list(
    mock_pending_job: Job,
    mock_running_job: Job,
    mock_completed_job: Job,
    mock_failed_job: Job,
) -> list[Job]:
    """Provide a list of mock jobs in various states."""
    return [
        mock_pending_job,
        mock_running_job,
        mock_completed_job,
        mock_failed_job,
    ]


# =============================================================================
# Queue with Dependencies Fixtures
# =============================================================================


@pytest.fixture
def queue_with_jobs(in_memory_queue: JobQueue, sample_payload: dict[str, Any]) -> JobQueue:
    """Provide a queue pre-populated with various jobs."""
    # Add jobs in different states
    in_memory_queue.enqueue(sample_payload, priority=JobPriority.HIGH)
    in_memory_queue.enqueue(sample_payload, priority=JobPriority.NORMAL)
    in_memory_queue.enqueue(sample_payload, priority=JobPriority.LOW)

    # Mark one as running
    in_memory_queue.dequeue()

    # Complete one
    job4 = in_memory_queue.enqueue(sample_payload)
    if job4:
        in_memory_queue.complete(job4.id)

    # Fail one
    job5 = in_memory_queue.enqueue(sample_payload)
    if job5:
        in_memory_queue.fail(job5.id, "Test error")

    return in_memory_queue


@pytest.fixture
def queue_with_dependencies(in_memory_queue: JobQueue, sample_payload: dict[str, Any]) -> JobQueue:
    """Provide a queue with jobs that have dependencies."""
    # Create parent job
    parent = in_memory_queue.enqueue(sample_payload, priority=JobPriority.HIGH)

    # Create child job that depends on parent
    if parent and parent.id:
        in_memory_queue.enqueue(
            {"task": "child_task"},
            priority=JobPriority.NORMAL,
            depends_on=[parent.id],
        )

    return in_memory_queue


# =============================================================================
# Rate Limiting Fixtures
# =============================================================================


@pytest.fixture
def rate_limit_config() -> RateLimitConfig:
    """Provide a basic rate limit configuration."""
    return RateLimitConfig(
        rate=10.0,  # 10 requests per second
        burst=10,  # Max 10 concurrent
    )


@pytest.fixture
def rate_limiter(rate_limit_config: RateLimitConfig) -> RateLimiter:
    """Provide a rate limiter with default configuration."""
    return RateLimiter(rate_limit_config)


@pytest.fixture
def queue_with_rate_limit(
    temp_db_path: Path, rate_limiter: RateLimiter
) -> Generator[JobQueue, None, None]:
    """Provide a queue with rate limiting enabled."""
    queue = JobQueue(
        temp_db_path,
        rate_limiter=rate_limiter,
        rate_limit_behavior="block",
    )
    yield queue
    queue.close()


# =============================================================================
# Dead Letter Queue Fixtures
# =============================================================================


@pytest.fixture
def temp_dlq_path() -> Generator[Path, None, None]:
    """Create a temporary DLQ database file path."""
    with tempfile.NamedTemporaryFile(suffix="_dlq.db", delete=False) as f:
        path = Path(f.name)
    yield path
    if path.exists():
        path.unlink()


@pytest.fixture
def dead_letter_queue(temp_dlq_path: Path) -> Generator[DeadLetterQueue, None, None]:
    """Provide a dead letter queue for testing."""
    dlq = DeadLetterQueue(temp_dlq_path)
    yield dlq
    dlq.close()


@pytest.fixture
def queue_with_dlq(temp_db_path: Path, temp_dlq_path: Path) -> Generator[JobQueue, None, None]:
    """Provide a queue with a dead letter queue attached."""
    dlq = DeadLetterQueue(temp_dlq_path)
    queue = JobQueue(
        temp_db_path,
        dlq=dlq,
        max_retries=2,
    )
    yield queue
    queue.close()
    dlq.close()


# =============================================================================
# Encryption Fixtures
# =============================================================================


@pytest.fixture
def encryption_manager() -> Generator[EncryptionManager, None, None]:
    """Provide an encryption manager with a test key."""
    # Use a fixed test key for reproducibility (exactly 32 bytes for AES-256)
    test_key = "test-encryption-key-32-bytes!!!!"
    manager = EncryptionManager(test_key)
    yield manager


@pytest.fixture
def queue_with_encryption(
    temp_db_path: Path, encryption_manager: EncryptionManager
) -> Generator[JobQueue, None, None]:
    """Provide a queue with encryption enabled."""
    queue = JobQueue(
        temp_db_path,
        encryption_manager=encryption_manager,
    )
    yield queue
    queue.close()


# =============================================================================
# Mock Callback Fixtures
# =============================================================================


@pytest.fixture
def mock_callback() -> MagicMock:
    """Provide a mock callback function."""
    return MagicMock()


@pytest.fixture
def mock_dlq_callback() -> MagicMock:
    """Provide a mock DLQ callback function."""
    return MagicMock()


# =============================================================================
# Configuration Fixtures
# =============================================================================


@pytest.fixture
def mock_config() -> dict[str, Any]:
    """Provide a mock configuration dictionary."""
    return {
        "database": {
            "path": ":memory:",
            "max_connections": 10,
        },
        "queue": {
            "max_retries": 3,
            "base_delay_seconds": 5,
            "max_delay_seconds": 3600,
        },
        "rate_limit": {
            "enabled": True,
            "max_requests": 100,
            "window_seconds": 60,
        },
        "encryption": {
            "enabled": False,
            "key_path": None,
        },
    }


# =============================================================================
# Pytest Configuration
# =============================================================================


def pytest_configure(config):
    """Configure pytest with custom markers."""
    config.addinivalue_line(
        "markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')"
    )
    config.addinivalue_line("markers", "integration: marks tests as integration tests")
    config.addinivalue_line("markers", "unit: marks tests as unit tests")
    config.addinivalue_line("markers", "encryption: marks tests that require encryption")
    config.addinivalue_line("markers", "database: marks tests that require a database")


# =============================================================================
# Async Database Fixtures
# =============================================================================


@pytest_asyncio.fixture
async def async_in_memory_store() -> AsyncGenerator[JobStore, None]:
    """Provide an async-compatible in-memory job store."""
    store = JobStore(":memory:")
    yield store
    store.close()


@pytest_asyncio.fixture
async def async_in_memory_queue() -> AsyncGenerator[JobQueue, None]:
    """Provide an async-compatible in-memory job queue."""
    queue = JobQueue(":memory:")
    yield queue
    queue.close()


# =============================================================================
# Cache Fixtures
# =============================================================================


@pytest.fixture
def cache_config() -> CacheConfig:
    """Provide a basic cache configuration."""
    return CacheConfig(
        default_ttl=3600,
        max_size=1000,
    )


@pytest.fixture
def in_memory_cache() -> Generator[ResultCache, None, None]:
    """Provide an in-memory result cache for testing."""
    cache = ResultCache(CacheConfig())
    yield cache
    cache.close()


@pytest.fixture
def temp_cache_path() -> Generator[Path, None, None]:
    """Create a temporary cache database file path."""
    with tempfile.NamedTemporaryFile(suffix="_cache.db", delete=False) as f:
        path = Path(f.name)
    yield path
    if path.exists():
        path.unlink()


@pytest.fixture
def file_backed_cache(temp_cache_path: Path) -> Generator[ResultCache, None, None]:
    """Provide a file-backed result cache for testing."""
    config = CacheConfig(db_path=temp_cache_path)
    cache = ResultCache(config)
    yield cache
    cache.close()


@pytest.fixture
def queue_with_cache(
    temp_db_path: Path,
) -> Generator[JobQueue, None, None]:
    """Provide a queue with result caching enabled."""
    cache_config = CacheConfig(default_ttl=3600, max_size=1000)
    queue = JobQueue(
        temp_db_path,
        result_cache=cache_config,
    )
    yield queue
    queue.close()


@pytest.fixture
def small_cache_config() -> CacheConfig:
    """Provide a small cache config for testing eviction."""
    return CacheConfig(
        default_ttl=3600,
        max_size=5,
    )


@pytest.fixture
def small_cache(small_cache_config: CacheConfig) -> Generator[ResultCache, None, None]:
    """Provide a small cache for testing LRU eviction."""
    cache = ResultCache(small_cache_config)
    yield cache
    cache.close()
