"""Integration tests for Dead Letter Queue (DLQ) features.

Tests end-to-end DLQ workflows including:
- Job failure handling
- DLQ entry management
- Retry from DLQ
- DLQ persistence
"""

from __future__ import annotations

import pytest

from oclawma.queue.dlq import DeadLetterQueue
from oclawma.queue.models import Job, JobPriority, JobStatus
from oclawma.queue.queue import JobQueue


@pytest.mark.integration
class TestDeadLetterQueueWorkflow:
    """End-to-end tests for DLQ workflows."""

    def test_job_moved_to_dlq_after_exhausted_retries(
        self, queue_with_dlq: JobQueue, dead_letter_queue: DeadLetterQueue
    ):
        """Test that jobs are moved to DLQ after exhausting retries."""
        queue = queue_with_dlq

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

        # First attempt: dequeue, fail, then retry
        dequeued, _ = queue.dequeue_with_rate_limit()
        assert dequeued is not None
        queue.fail(dequeued.id, "Failure 1")

        # Explicitly retry the job
        queue.retry(job_id)

        # Verify job is now pending but scheduled for future (has scheduled_at)
        job_after_retry = queue.get_job(job_id)
        assert job_after_retry.status.value == "pending"
        assert job_after_retry.retry_count == 1

        # Manually reset scheduled_at to now so we can dequeue it immediately
        # (In real scenarios, we'd wait for the backoff delay)
        job_after_retry.scheduled_at = None
        queue.store.update(job_after_retry)

        # Second attempt: dequeue again and fail (exhausts retries)
        dequeued2, _ = queue.dequeue_with_rate_limit()
        assert dequeued2 is not None
        queue.fail(dequeued2.id, "Failure 2")

        # Verify job is in failed state in main queue (no more retries)
        failed_job = queue.get_job(job_id)
        assert failed_job.status.value == "failed"
        assert not failed_job.is_retryable()

    def test_dlq_entry_contains_failure_info(self, dead_letter_queue: DeadLetterQueue):
        """Test that DLQ entries contain failure information."""
        dlq = dead_letter_queue

        # Create a job and add to DLQ
        job = Job(
            id=123,
            payload={"task": "failed"},
            status=JobStatus.FAILED,
        )
        error = Exception("Something went wrong")
        context = {"worker_id": "worker-1", "attempt": 3}

        # Add to DLQ
        dlq.add(job, error, context=context)

        # Retrieve and verify
        entries = dlq.list_entries()
        assert len(entries) == 1

        entry = entries[0]
        assert entry.original_job_id == 123
        assert entry.error_message == "Something went wrong"
        assert entry.context == context
        assert entry.retry_count == 3

    def test_dlq_persists_across_sessions(self, temp_dlq_path):
        """Test that DLQ entries persist across sessions."""
        from oclawma.queue.dlq import DeadLetterQueue
        from oclawma.queue.models import Job, JobStatus

        # Create first DLQ instance
        dlq1 = DeadLetterQueue(temp_dlq_path)

        job = Job(id=1, payload={"task": "persist"}, status=JobStatus.FAILED)
        dlq1.add(job, Exception("Test"))
        dlq1.close()

        # Create second DLQ instance
        dlq2 = DeadLetterQueue(temp_dlq_path)

        entries = dlq2.list_entries()
        assert len(entries) == 1
        assert entries[0].original_job_id == 1
        dlq2.close()

    def test_dlq_stats(self, dead_letter_queue: DeadLetterQueue):
        """Test DLQ statistics."""
        dlq = dead_letter_queue

        # Add entries with different error types
        for i in range(5):
            job = Job(id=i, payload={"index": i}, status=JobStatus.FAILED)
            if i < 3:
                dlq.add(job, Exception("NetworkError"))
            else:
                dlq.add(job, Exception("ValidationError"))

        stats = dlq.get_stats()

        assert stats["total"] == 5
        assert stats["unresolved"] == 5
        assert stats["resolved"] == 0

        # Check error type breakdown
        assert "NetworkError" in stats["by_error_type"]
        assert "ValidationError" in stats["by_error_type"]
        assert stats["by_error_type"]["NetworkError"] == 3
        assert stats["by_error_type"]["ValidationError"] == 2

    def test_dlq_entry_resolution(self, dead_letter_queue: DeadLetterQueue):
        """Test marking DLQ entries as resolved."""
        dlq = dead_letter_queue

        # Add entry
        job = Job(id=1, payload={"task": "fixable"}, status=JobStatus.FAILED)
        entry_id = dlq.add(job, Exception("FixableError"))

        # Mark as resolved
        dlq.mark_resolved(entry_id, resolution_notes="Fixed by retry")

        # Verify
        entries = dlq.list_entries(resolved=False)
        assert len(entries) == 0

        resolved = dlq.list_entries(resolved=True)
        assert len(resolved) == 1
        assert resolved[0].resolution_notes == "Fixed by retry"

    def test_dlq_retry_job_recreation(self, dead_letter_queue: DeadLetterQueue):
        """Test recreating a job from DLQ for retry."""
        dlq = dead_letter_queue

        # Add entry
        original_job = Job(
            id=100,
            payload={"task": "retry_me"},
            status=JobStatus.FAILED,
            priority=JobPriority.HIGH,
            max_retries=3,
        )
        entry_id = dlq.add(original_job, Exception("TemporaryError"))

        # Get entry and recreate job
        entry = dlq.get_entry(entry_id)

        # Create new job with same payload
        new_job = Job(
            payload=entry.payload,
            status=JobStatus.PENDING,
            priority=entry.priority,
            max_retries=entry.max_retries,
        )

        assert new_job.payload == original_job.payload
        assert new_job.priority == original_job.priority
        assert new_job.status == JobStatus.PENDING

    def test_dlq_cleanup_old_entries(self, dead_letter_queue: DeadLetterQueue):
        """Test cleaning up old resolved DLQ entries."""
        dlq = dead_letter_queue

        # Add and resolve some entries
        for i in range(5):
            job = Job(id=i, payload={"index": i}, status=JobStatus.FAILED)
            entry_id = dlq.add(job, Exception("OldError"))
            dlq.mark_resolved(entry_id)

        # Cleanup entries older than 0 days (all of them)
        deleted = dlq.cleanup_old_entries(max_age_days=0, resolved_only=True)

        assert deleted == 5
        assert len(dlq.list_entries()) == 0


@pytest.mark.integration
class TestDLQWithMainQueue:
    """Integration tests for DLQ working with main job queue."""

    def test_end_to_end_failure_to_dlq(self, queue_with_dlq: JobQueue, temp_dlq_path):
        """Test complete workflow from failure to DLQ."""
        from oclawma.queue.dlq import DeadLetterQueue

        queue = queue_with_dlq
        dlq = DeadLetterQueue(temp_dlq_path)

        # Set up DLQ callback
        dlq_entries = []

        def on_dlq_callback(job, error):
            dlq_entries.append((job.id, str(error)))
            dlq.add(job, error)

        queue.on_dlq = on_dlq_callback

        # Create and fail a job
        job = queue.enqueue({"task": "failing"}, max_retries=0)
        job_id = job.id

        dequeued, _ = queue.dequeue_with_rate_limit()
        if dequeued:
            queue.fail(dequeued.id, "Permanent failure")

        # Verify callback was called
        assert len(dlq_entries) == 1
        assert dlq_entries[0][0] == job_id

        dlq.close()

    def test_dlq_with_different_error_types(self, dead_letter_queue: DeadLetterQueue):
        """Test DLQ handles different error types correctly."""
        dlq = dead_letter_queue

        # Add entries with various error types
        errors = [
            ValueError("Invalid value"),
            RuntimeError("Runtime problem"),
            ConnectionError("Network failed"),
            TimeoutError("Timed out"),
        ]

        for i, error in enumerate(errors):
            job = Job(id=i, payload={"index": i}, status=JobStatus.FAILED)
            dlq.add(job, error)

        # Verify all errors are captured
        entries = dlq.list_entries()
        assert len(entries) == 4

        error_types = {entry.error_type for entry in entries}
        assert "ValueError" in error_types
        assert "RuntimeError" in error_types
        assert "ConnectionError" in error_types
        assert "TimeoutError" in error_types
