"""Tests for the Dead Letter Queue (DLQ) module."""

from __future__ import annotations

from datetime import datetime, timedelta

import pytest

from oclawma.queue import Job, JobStatus
from oclawma.queue.dlq import (
    DeadLetterQueue,
    DLQEntry,
    DLQNotFoundError,
    DLQStats,
)


class TestDLQEntryModel:
    """Test DLQEntry Pydantic model."""

    def test_default_creation(self):
        """Test creating a DLQ entry with defaults."""
        job = Job(payload={"task": "test"})
        entry = DLQEntry(
            job=job,
            failure_reason="Test error",
        )

        assert entry.id is None
        assert entry.job == job
        assert entry.failure_reason == "Test error"
        assert entry.error_message is None  # Defaults to None
        assert entry.stack_trace is None
        assert entry.retry_count == 0
        assert isinstance(entry.failed_at, datetime)

    def test_custom_creation(self):
        """Test creating a DLQ entry with custom values."""
        job = Job(
            payload={"task": "test"},
            retry_count=3,
            max_retries=3,
        )
        failed_at = datetime.utcnow()

        entry = DLQEntry(
            id=1,
            job=job,
            failure_reason="ValueError",
            error_message="Something went wrong",
            stack_trace="Traceback (most recent call last): ...",
            retry_count=3,
            failed_at=failed_at,
        )

        assert entry.id == 1
        assert entry.job == job
        assert entry.failure_reason == "ValueError"
        assert entry.error_message == "Something went wrong"
        assert entry.stack_trace == "Traceback (most recent call last): ..."
        assert entry.retry_count == 3
        assert entry.failed_at == failed_at

    def test_to_from_db_dict(self):
        """Test database serialization round-trip."""
        original_job = Job(
            id=42,
            payload={"task": "test", "data": [1, 2, 3]},
            status=JobStatus.FAILED,
            retry_count=3,
            max_retries=3,
            error="Test error",
        )
        original = DLQEntry(
            id=1,
            job=original_job,
            failure_reason="RuntimeError",
            error_message="Something failed",
            stack_trace="Traceback: line 10",
            retry_count=3,
        )

        db_dict = original.to_db_dict()
        restored = DLQEntry.from_db_row(db_dict)

        assert restored.id == original.id
        assert restored.job.payload == original.job.payload
        assert restored.failure_reason == original.failure_reason
        assert restored.error_message == original.error_message
        assert restored.stack_trace == original.stack_trace
        assert restored.retry_count == original.retry_count


class TestDLQStats:
    """Test DLQStats model."""

    def test_default_creation(self):
        """Test default creation."""
        stats = DLQStats()
        assert stats.total == 0
        assert stats.by_error_type == {}
        assert stats.oldest is None
        assert stats.newest is None

    def test_custom_creation(self):
        """Test custom creation."""
        now = datetime.utcnow()
        stats = DLQStats(
            total=5,
            by_error_type={"ValueError": 2, "RuntimeError": 3},
            oldest=now - timedelta(days=5),
            newest=now,
        )
        assert stats.total == 5
        assert stats.by_error_type == {"ValueError": 2, "RuntimeError": 3}
        assert stats.oldest == now - timedelta(days=5)
        assert stats.newest == now


class TestDeadLetterQueueBasic:
    """Test basic DLQ operations."""

    @pytest.fixture
    def dlq(self, tmp_path):
        """Create a temporary DLQ for testing."""
        db_path = tmp_path / "test_dlq.db"
        with DeadLetterQueue(db_path) as dlq:
            yield dlq

    def test_add(self, dlq):
        """Test adding a job to DLQ."""
        job = Job(payload={"task": "test"}, retry_count=3, max_retries=3)

        entry_id = dlq.add(
            job=job,
            error=Exception("Test error"),
        )

        # add() returns the entry ID
        assert entry_id is not None

        # Get the full entry to verify details
        entry = dlq.get(entry_id)
        assert entry.job.payload == {"task": "test"}
        # For generic Exception, failure_reason is extracted from message
        assert entry.failure_reason == "Test error"
        assert entry.error_message == "Test error"
        assert entry.retry_count == 3

    def test_add_with_error_details(self, dlq):
        """Test adding a job with full error details."""
        job = Job(payload={"task": "test"})

        try:
            raise ValueError("Something went wrong")
        except Exception as e:
            import traceback

            tb = traceback.format_exc()
            entry_id = dlq.add(
                job=job,
                error=e,
                stack_trace=tb,
            )

        # add() returns the entry ID, get full entry to verify
        entry = dlq.get(entry_id)
        # For specific exceptions, failure_reason is the class name
        assert entry.failure_reason == "ValueError"
        assert entry.error_message == "Something went wrong"
        assert entry.stack_trace is not None
        assert "Traceback" in entry.stack_trace

    def test_list_empty(self, dlq):
        """Test listing empty DLQ."""
        entries = dlq.list()
        assert entries == []

    def test_list_with_entries(self, dlq):
        """Test listing DLQ entries."""
        job1 = Job(payload={"task": "first"})
        job2 = Job(payload={"task": "second"})

        dlq.add(job1, Exception("Error 1"))
        dlq.add(job2, ValueError("Error 2"))

        entries = dlq.list()

        assert len(entries) == 2
        assert entries[0].job.payload == {"task": "first"}
        assert entries[1].job.payload == {"task": "second"}

    def test_list_with_limit(self, dlq):
        """Test listing with limit."""
        for i in range(5):
            job = Job(payload={"task": f"job_{i}"})
            dlq.add(job, Exception(f"Error {i}"))

        entries = dlq.list(limit=3)

        assert len(entries) == 3

    def test_get(self, dlq):
        """Test getting a DLQ entry by id."""
        job = Job(payload={"task": "test"})
        entry_id = dlq.add(job, Exception("Test error"))

        retrieved = dlq.get(entry_id)

        assert retrieved.id == entry_id
        assert retrieved.job.payload == job.payload

    def test_get_not_found(self, dlq):
        """Test getting non-existent entry."""
        with pytest.raises(DLQNotFoundError):
            dlq.get(999)

    def test_delete(self, dlq):
        """Test deleting a DLQ entry."""
        job = Job(payload={"task": "test"})
        entry_id = dlq.add(job, Exception("Test error"))

        result = dlq.delete(entry_id)

        assert result is True

        # Entry should be gone
        with pytest.raises(DLQNotFoundError):
            dlq.get(entry_id)

    def test_delete_not_found(self, dlq):
        """Test deleting non-existent entry."""
        result = dlq.delete(999)
        assert result is False


class TestDeadLetterQueueReplay:
    """Test DLQ replay functionality."""

    @pytest.fixture
    def dlq(self, tmp_path):
        """Create a temporary DLQ for testing."""
        db_path = tmp_path / "test_dlq.db"
        with DeadLetterQueue(db_path) as dlq:
            yield dlq

    def test_replay(self, dlq):
        """Test replaying a job from DLQ."""
        original_job = Job(
            payload={"task": "test"},
            retry_count=3,
            max_retries=3,
        )
        entry_id = dlq.add(original_job, Exception("Test error"))

        replayed_job = dlq.replay(entry_id)

        assert replayed_job is not None
        assert replayed_job.payload == {"task": "test"}
        assert replayed_job.status == JobStatus.PENDING
        assert replayed_job.retry_count == 0  # Reset
        assert replayed_job.error is None
        # New job has no ID yet (not persisted), original had no ID either
        # The key point is replay creates a fresh job ready to be queued

    def test_replay_deletes_entry(self, dlq):
        """Test that replay removes the DLQ entry."""
        job = Job(payload={"task": "test"})
        entry_id = dlq.add(job, Exception("Test error"))

        dlq.replay(entry_id)

        # Entry should be deleted
        with pytest.raises(DLQNotFoundError):
            dlq.get(entry_id)

    def test_replay_not_found(self, dlq):
        """Test replaying non-existent entry."""
        with pytest.raises(DLQNotFoundError):
            dlq.replay(999)

    def test_replay_all(self, dlq):
        """Test replaying all jobs from DLQ."""
        job1 = Job(payload={"task": "first"})
        job2 = Job(payload={"task": "second"})

        dlq.add(job1, Exception("Error 1"))
        dlq.add(job2, ValueError("Error 2"))

        replayed = dlq.replay_all()

        assert len(replayed) == 2
        assert all(j.status == JobStatus.PENDING for j in replayed)

        # DLQ should be empty
        assert len(dlq.list()) == 0

    def test_replay_all_empty(self, dlq):
        """Test replaying all from empty DLQ."""
        replayed = dlq.replay_all()
        assert replayed == []


class TestDeadLetterQueueStats:
    """Test DLQ statistics."""

    @pytest.fixture
    def dlq(self, tmp_path):
        """Create a temporary DLQ for testing."""
        db_path = tmp_path / "test_dlq.db"
        with DeadLetterQueue(db_path) as dlq:
            yield dlq

    def test_stats_empty(self, dlq):
        """Test stats for empty DLQ."""
        stats = dlq.get_stats()

        assert stats["total"] == 0
        assert stats["by_error_type"] == {}

    def test_stats_with_entries(self, dlq):
        """Test stats with various error types."""
        job1 = Job(payload={"task": "first"})
        job2 = Job(payload={"task": "second"})
        job3 = Job(payload={"task": "third"})

        dlq.add(job1, ValueError("Error 1"))
        dlq.add(job2, ValueError("Error 2"))
        dlq.add(job3, RuntimeError("Error 3"))

        stats = dlq.get_stats()

        assert stats["total"] == 3
        assert stats["by_error_type"] == {"ValueError": 2, "RuntimeError": 1}


class TestDeadLetterQueuePersistence:
    """Test DLQ persistence across instances."""

    def test_persistence(self, tmp_path):
        """Test entries persist across DLQ instances."""
        db_path = tmp_path / "persist.db"

        # Create DLQ and add entry
        with DeadLetterQueue(db_path) as dlq1:
            job = Job(payload={"task": "persist"})
            entry_id = dlq1.add(job, Exception("Test error"))

        # Create new DLQ instance with same DB
        with DeadLetterQueue(db_path) as dlq2:
            retrieved = dlq2.get(entry_id)
            assert retrieved.job.payload == {"task": "persist"}


class TestDeadLetterQueueIntegration:
    """Test DLQ integration with JobQueue."""

    def test_dlq_with_same_database(self, tmp_path):
        """Test DLQ and JobQueue can share a database."""
        from oclawma.queue import JobQueue

        db_path = tmp_path / "shared.db"

        with JobQueue(db_path) as queue, DeadLetterQueue(db_path) as dlq:
            # Add a job to the queue
            _ = queue.enqueue({"task": "test"})

            # Fail the job
            dequeued = queue.dequeue()
            failed = queue.fail(dequeued.id, "Test error")

            # Add to DLQ
            _ = dlq.add(failed, Exception("Test error"))

            # Verify both work
            assert queue.list_jobs()
            assert dlq.list()

    def test_replay_puts_job_back_in_queue(self, tmp_path):
        """Test replaying puts job back into a job queue."""
        from oclawma.queue import JobQueue

        db_path = tmp_path / "shared.db"

        with JobQueue(db_path) as queue, DeadLetterQueue(db_path) as dlq:
            # Add failed job to DLQ
            job = Job(payload={"task": "replay_me"}, status=JobStatus.FAILED)
            entry_id = dlq.add(job, Exception("Test error"))

            # Replay and get the new job
            replayed_job = dlq.replay(entry_id)

            # Add to queue manually (replay just creates a clean job)
            new_job = queue.enqueue(
                replayed_job.payload,
                max_retries=replayed_job.max_retries,
            )

            assert new_job.status == JobStatus.PENDING
            assert new_job.payload == {"task": "replay_me"}
