"""Integration tests for OCLAWMA job queue.

These tests verify end-to-end queue functionality including:
- Job lifecycle (enqueue, dequeue, complete, fail)
- Priority scheduling
- Delayed execution
- Retry mechanisms
- Dead letter queue integration
- Rate limiting
- Encryption
"""

from __future__ import annotations

from datetime import datetime, timedelta
from typing import Any

import pytest

from oclawma.queue.models import JobPriority, JobStatus
from oclawma.queue.queue import JobQueue
from oclawma.ratelimit import RateLimitConfig, RateLimiter


@pytest.mark.integration
class TestJobLifecycle:
    """End-to-end tests for job lifecycle operations."""

    def test_full_job_lifecycle(self, in_memory_queue: JobQueue, sample_payload: dict[str, Any]):
        """Test complete job lifecycle from enqueue to completion."""
        # Enqueue a job
        job = in_memory_queue.enqueue(sample_payload)
        assert job is not None
        assert job.id is not None
        assert job.status == JobStatus.PENDING

        # Verify it's in the queue
        stats = in_memory_queue.get_stats()
        assert stats.pending == 1

        # Dequeue the job
        dequeued_job, _ = in_memory_queue.dequeue_with_rate_limit()
        assert dequeued_job is not None
        assert dequeued_job.id == job.id
        assert dequeued_job.status == JobStatus.RUNNING

        # Complete the job
        result = in_memory_queue.complete(job.id)
        assert result.status == JobStatus.COMPLETED

        # Verify completion
        completed_job = in_memory_queue.get_job(job.id)
        assert completed_job.status == JobStatus.COMPLETED
        assert completed_job.completed_at is not None

        stats = in_memory_queue.get_stats()
        assert stats.completed == 1
        assert stats.pending == 0

    def test_job_failure_and_retry(self, in_memory_queue: JobQueue, sample_payload: dict[str, Any]):
        """Test job failure and retry mechanism."""
        # Enqueue with custom retry settings
        job = in_memory_queue.enqueue(
            sample_payload,
            max_retries=2,
        )
        assert job is not None
        job_id = job.id

        # First attempt - fail, then retry
        dequeued_job, _ = in_memory_queue.dequeue_with_rate_limit()
        assert dequeued_job is not None
        in_memory_queue.fail(job_id, "First failure")

        # Explicitly retry the job
        in_memory_queue.retry(job_id)

        # Verify retry scheduled
        failed_job = in_memory_queue.get_job(job_id)
        assert failed_job.retry_count == 1
        assert failed_job.status == JobStatus.PENDING  # Re-queued for retry

        # Second attempt - fail again, then retry
        dequeued_job, _ = in_memory_queue.dequeue_with_rate_limit()
        assert dequeued_job is not None
        in_memory_queue.fail(job_id, "Second failure")

        # Explicitly retry the job
        in_memory_queue.retry(job_id)

        # Third attempt - fail (exhausted retries)
        dequeued_job, _ = in_memory_queue.dequeue_with_rate_limit()
        assert dequeued_job is not None
        in_memory_queue.fail(job_id, "Third failure")

        # Verify exhausted retries (no more retries available)
        failed_job = in_memory_queue.get_job(job_id)
        assert failed_job.retry_count == 2  # max_retries=2, so count stops at 2
        assert failed_job.status == JobStatus.FAILED
        assert not failed_job.is_retryable()

    def test_multiple_jobs_processing(
        self, in_memory_queue: JobQueue, sample_payload: dict[str, Any]
    ):
        """Test processing multiple jobs in sequence."""
        # Enqueue multiple jobs
        jobs = []
        for i in range(5):
            job = in_memory_queue.enqueue({**sample_payload, "index": i})
            jobs.append(job)

        # Process all jobs
        processed = []
        for _ in range(5):
            job, _ = in_memory_queue.dequeue_with_rate_limit()
            if job:
                in_memory_queue.complete(job.id)
                processed.append(job.id)

        assert len(processed) == 5

        # Verify all completed
        stats = in_memory_queue.get_stats()
        assert stats.completed == 5
        assert stats.pending == 0


@pytest.mark.integration
class TestPriorityScheduling:
    """End-to-end tests for priority-based job scheduling."""

    def test_priority_ordering(self, in_memory_queue: JobQueue):
        """Test that jobs are dequeued in priority order."""
        # Enqueue jobs with different priorities
        in_memory_queue.enqueue({"task": "low"}, priority=JobPriority.LOW)
        in_memory_queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)
        in_memory_queue.enqueue({"task": "high"}, priority=JobPriority.HIGH)
        in_memory_queue.enqueue({"task": "critical"}, priority=JobPriority.CRITICAL)

        # Dequeue should return in priority order
        jobs = []
        for _ in range(4):
            job, _ = in_memory_queue.dequeue_with_rate_limit()
            if job:
                jobs.append(job)
                in_memory_queue.complete(job.id)

        # Verify priority order
        priorities = [job.priority for job in jobs]
        assert priorities == [
            JobPriority.CRITICAL,
            JobPriority.HIGH,
            JobPriority.NORMAL,
            JobPriority.LOW,
        ]

    def test_same_priority_fifo(self, in_memory_queue: JobQueue):
        """Test FIFO ordering for jobs with same priority."""
        # Enqueue multiple jobs with same priority
        job_ids = []
        for i in range(5):
            job = in_memory_queue.enqueue({"task": f"job_{i}"}, priority=JobPriority.NORMAL)
            job_ids.append(job.id)

        # Dequeue all
        dequeued_ids = []
        for _ in range(5):
            job, _ = in_memory_queue.dequeue_with_rate_limit()
            if job:
                dequeued_ids.append(job.id)
                in_memory_queue.complete(job.id)

        # Should be in FIFO order
        assert dequeued_ids == job_ids


@pytest.mark.integration
class TestDelayedExecution:
    """End-to-end tests for delayed job execution."""

    def test_scheduled_job_not_ready(self, in_memory_queue: JobQueue):
        """Test that scheduled jobs aren't dequeued before their time."""
        future_time = datetime.utcnow() + timedelta(minutes=5)

        in_memory_queue.enqueue(
            {"task": "future"},
            scheduled_at=future_time,
        )

        # Try to dequeue - should get None
        dequeued_job, _ = in_memory_queue.dequeue_with_rate_limit()
        assert dequeued_job is None

        # Job should still be pending
        stats = in_memory_queue.get_stats()
        assert stats.pending == 1

    def test_scheduled_job_ready(self, in_memory_queue: JobQueue):
        """Test that scheduled jobs are dequeued when their time comes."""
        past_time = datetime.utcnow() - timedelta(minutes=1)

        job = in_memory_queue.enqueue(
            {"task": "past"},
            scheduled_at=past_time,
        )

        # Should be dequeued immediately
        dequeued_job, _ = in_memory_queue.dequeue_with_rate_limit()
        assert dequeued_job is not None
        assert dequeued_job.id == job.id


@pytest.mark.integration
class TestJobDependencies:
    """End-to-end tests for job dependencies."""

    def test_dependent_job_blocked(self, queue_with_dependencies: JobQueue):
        """Test that dependent jobs are blocked until dependencies complete."""
        queue = queue_with_dependencies

        # Get the parent job (should be dequeued first)
        parent, _ = queue.dequeue_with_rate_limit()
        assert parent is not None

        # Try to get next job - should be None (child is blocked)
        child, _ = queue.dequeue_with_rate_limit()
        assert child is None

        # Complete parent
        queue.complete(parent.id)

        # Now child should be available
        child, _ = queue.dequeue_with_rate_limit()
        assert child is not None
        assert child.payload.get("task") == "child_task"

    def test_dependency_chain(self, in_memory_queue: JobQueue):
        """Test a chain of dependent jobs."""
        # Create job chain: A -> B -> C
        job_a = in_memory_queue.enqueue({"name": "A"})
        job_b = in_memory_queue.enqueue({"name": "B"}, depends_on=[job_a.id])
        in_memory_queue.enqueue({"name": "C"}, depends_on=[job_b.id])

        # Process in order
        for expected_name in ["A", "B", "C"]:
            job, _ = in_memory_queue.dequeue_with_rate_limit()
            assert job is not None
            assert job.payload.get("name") == expected_name
            in_memory_queue.complete(job.id)


@pytest.mark.integration
class TestDeadLetterQueue:
    """End-to-end tests for dead letter queue integration."""

    def test_job_moved_to_dlq_after_retries(self, queue_with_dlq: JobQueue, mock_dlq_callback):
        """Test that exhausted jobs are moved to DLQ."""
        queue = queue_with_dlq

        # Create a job that will exhaust retries
        job = queue.enqueue({"task": "failing"}, max_retries=1)
        job_id = job.id

        # Fail it twice (exhausting retries)
        for _ in range(2):
            dequeued, _ = queue.dequeue_with_rate_limit()
            if dequeued:
                queue.fail(dequeued.id, "Failure")

        # Job should be in failed state
        failed_job = queue.get_job(job_id)
        assert failed_job.status == JobStatus.FAILED

    def test_dlq_persists_failed_jobs(
        self, temp_queue: JobQueue, temp_dlq_path, sample_payload: dict[str, Any]
    ):
        """Test that DLQ persists failed jobs."""
        from oclawma.queue.dlq import DeadLetterQueue

        dlq = DeadLetterQueue(temp_dlq_path)

        # Create a queue with DLQ
        queue = JobQueue(
            temp_queue.store.db_path,
            dlq=dlq,
            max_retries=0,  # No retries, immediate DLQ
        )

        # Add and fail a job
        job = queue.enqueue(sample_payload)
        dequeued, _ = queue.dequeue_with_rate_limit()

        # Add to DLQ manually
        dlq.add(dequeued, Exception("Test error"), context={"test": True})

        # Verify in DLQ
        entries = dlq.list_entries()
        assert len(entries) == 1
        assert entries[0].original_job_id == job.id

        dlq.close()
        queue.close()


@pytest.mark.integration
class TestRateLimiting:
    """End-to-end tests for rate limiting integration."""

    def test_rate_limit_blocks_when_exceeded(self, queue_with_rate_limit: JobQueue):
        """Test that rate limiting blocks requests when limit exceeded."""
        queue = queue_with_rate_limit

        # Add a job with rate limit key (using job_type for rate limiting)
        queue.enqueue({"task": "limited"}, job_type="api_calls")

        # First dequeue should work
        dequeued1, info1 = queue.dequeue_with_rate_limit(job_type="api_calls")
        assert dequeued1 is not None

        # Add another job
        queue.enqueue({"task": "limited2"}, job_type="api_calls")

        # Second dequeue should be rate limited (burst=10, rate=10.0, so after consuming one, still available)
        # Actually, with burst=10, we can consume 10 tokens before being limited
        # Let's consume all tokens first
        for i in range(9):
            queue.enqueue({"task": f"limited_{i}"}, job_type="api_calls")

        # Now consume remaining tokens
        for _ in range(9):
            dequeued, _ = queue.dequeue_with_rate_limit(job_type="api_calls")
            if dequeued:
                queue.complete(dequeued.id)

        # Now the next dequeue should be rate limited
        dequeued2, info2 = queue.dequeue_with_rate_limit(job_type="api_calls")
        assert info2.allowed is False
        # Job should still be pending
        assert queue.get_stats().pending >= 1

    def test_rate_limit_configurable_per_key(self, temp_db_path):
        """Test that rate limits can be configured per key."""

        # Create rate limiter with different limits for different keys
        config = RateLimitConfig(
            rate=1.0,  # 1 request per second
            burst=1,  # Max 1 concurrent
        )
        limiter = RateLimiter(config)

        queue = JobQueue(temp_db_path, rate_limiter=limiter)

        # Add jobs with different keys
        queue.enqueue({"task": "api"}, job_type="api_call")
        queue.enqueue({"task": "batch"}, job_type="batch_job")

        # Both should be dequeueable (different job types)
        dequeued1, info1 = queue.dequeue_with_rate_limit(job_type="api_call")
        assert dequeued1 is not None
        assert info1.allowed is True

        # Complete first job to reset for clean state
        queue.complete(dequeued1.id)

        dequeued2, info2 = queue.dequeue_with_rate_limit(job_type="batch_job")
        assert dequeued2 is not None
        assert info2.allowed is True

        queue.close()


@pytest.mark.integration
class TestEncryption:
    """End-to-end tests for encryption integration."""

    def test_encrypted_job_payload(self, queue_with_encryption: JobQueue):
        """Test that job payloads can be encrypted and decrypted."""
        queue = queue_with_encryption

        sensitive_data = {"password": "secret123", "api_key": "sk-abc123"}

        # Enqueue encrypted job
        queue.enqueue(
            sensitive_data,
            job_type="sensitive",
            encrypted=True,
        )

        # Dequeue and verify
        dequeued, _ = queue.dequeue_with_rate_limit()
        assert dequeued is not None
        assert dequeued.encrypted is True
        assert dequeued.payload == sensitive_data

    def test_mixed_encrypted_and_plain(self, queue_with_encryption: JobQueue):
        """Test queue with both encrypted and plain jobs."""
        queue = queue_with_encryption

        # Add plain job
        queue.enqueue(
            {"task": "public"},
            encrypted=False,
        )

        # Add encrypted job
        queue.enqueue(
            {"task": "secret"},
            encrypted=True,
        )

        # Both should be processable
        jobs = []
        for _ in range(2):
            job, _ = queue.dequeue_with_rate_limit()
            if job:
                jobs.append(job)
                queue.complete(job.id)

        assert len(jobs) == 2

        # Verify encryption status
        encrypted_count = sum(1 for j in jobs if j.encrypted)
        plain_count = sum(1 for j in jobs if not j.encrypted)
        assert encrypted_count == 1
        assert plain_count == 1


@pytest.mark.integration
class TestQueuePersistence:
    """End-to-end tests for queue persistence across sessions."""

    def test_jobs_persist_after_reopen(self, temp_db_path):
        """Test that jobs persist when queue is reopened."""
        from oclawma.queue.queue import JobQueue

        # Create queue and add jobs
        queue1 = JobQueue(temp_db_path)
        queue1.enqueue({"task": "persist1"})
        queue1.enqueue({"task": "persist2"})
        queue1.close()

        # Reopen queue
        queue2 = JobQueue(temp_db_path)

        # Jobs should still be there
        stats = queue2.get_stats()
        assert stats.pending == 2

        # Should be able to dequeue
        job, _ = queue2.dequeue_with_rate_limit()
        assert job is not None
        assert job.payload.get("task") in ["persist1", "persist2"]

        queue2.close()

    def test_job_state_preserved(self, temp_db_path):
        """Test that job state is preserved across reopen."""
        from oclawma.queue.queue import JobQueue

        # Create and process some jobs
        queue1 = JobQueue(temp_db_path)
        job1 = queue1.enqueue({"task": "complete"})
        queue1.enqueue({"task": "pending"})

        # Complete one job
        queue1.complete(job1.id)
        queue1.close()

        # Reopen and verify states
        queue2 = JobQueue(temp_db_path)
        stats = queue2.get_stats()
        assert stats.completed == 1
        assert stats.pending == 1

        queue2.close()


@pytest.mark.integration
class TestQueueStats:
    """End-to-end tests for queue statistics."""

    def test_stats_accuracy(self, in_memory_queue: JobQueue):
        """Test that queue statistics are accurate."""
        queue = in_memory_queue

        # Add jobs that will complete
        for i in range(4):
            queue.enqueue({"task": f"complete_{i}"})

        # Add jobs that will fail (max_retries=0 so they don't auto-retry)
        for i in range(3):
            queue.enqueue({"task": f"fail_{i}"}, max_retries=0)

        # Add jobs that will stay pending
        for i in range(3):
            queue.enqueue({"task": f"pending_{i}"})

        # Complete some
        for _ in range(4):
            job, _ = queue.dequeue_with_rate_limit()
            if job:
                queue.complete(job.id)

        # Fail some
        for _ in range(3):
            job, _ = queue.dequeue_with_rate_limit()
            if job:
                queue.fail(job.id, "Test error")

        # Leave rest pending
        stats = queue.get_stats()
        assert stats.total_jobs == 10
        assert stats.completed == 4
        assert stats.failed == 3
        assert stats.pending == 3
        assert stats.running == 0

    def test_stats_with_running_jobs(self, in_memory_queue: JobQueue):
        """Test stats when jobs are running."""
        queue = in_memory_queue

        # Add and start jobs
        for i in range(5):
            queue.enqueue({"task": f"job_{i}"})

        # Start some but don't complete
        running_ids = []
        for _ in range(3):
            job, _ = queue.dequeue_with_rate_limit()
            if job:
                running_ids.append(job.id)

        queue.get_stats()
        # Jobs that were dequeued are marked as running in the database
        # But the queue's stats come from the database
        # Let's verify the store directly
        store_stats = queue.store.get_stats()
        assert store_stats["running"] == 3
        assert store_stats["pending"] == 2
