"""Test Dead Letter Queue persistence on permanent exporter failures.

CRITICAL: These tests verify that telemetry data is safely persisted to disk
when exporters fail permanently. Previous tests only verified retry behavior
but didn't confirm DLQ writes, risking data loss in production.

Addresses adversarial review finding: "The resilience test suite fails to validate
the graceful persistence of telemetry data into the Dead Letter Queue upon
permanent exporter failure, merely dropping items after 3 attempts."
"""

import pytest
import tempfile
import json
from pathlib import Path
from unittest.mock import Mock


class TestDLQPersistenceOnFailure:
    """Test that failed telemetry is persisted to DLQ after retry exhaustion."""

    def test_dlq_writes_to_disk_after_retry_exhaustion(self):
        """Test that DLQ persists failed items to disk after retries fail.

        CRITICAL: Validates that telemetry data is NOT lost when exporters
        fail permanently. Data must be written to disk for later recovery.
        """
        from mca_sdk.buffering.retry import RetryPolicy
        from mca_sdk.resilience.dead_letter_queue import DeadLetterQueue

        with tempfile.TemporaryDirectory() as tmpdir:
            dlq_path = Path(tmpdir) / "dlq.json"

            # Create DLQ with disk persistence
            dlq = DeadLetterQueue(queue_dir=str(dlq_path), max_size_mb=10)

            # Create retry policy with DLQ
            retry_policy = RetryPolicy(max_attempts=3, base_delay=0.01, dlq=dlq)  # Fast for testing

            # Function that always fails
            call_count = 0

            def always_fails():
                nonlocal call_count
                call_count += 1
                raise ConnectionError("Exporter unavailable")

            # Execute with retry - should exhaust attempts and write to DLQ
            with pytest.raises(ConnectionError):
                retry_policy.execute(always_fails)

            # Verify retries happened
            assert call_count == 3, f"Should have tried 3 times, got {call_count}"

            # CRITICAL: Verify DLQ persisted the failed item to disk
            assert dlq_path.exists(), "DLQ should have written failed item to disk"
            dlq_files = [dlq_path]

            # Verify file contains the failed item
            with open(dlq_files[0], "r") as f:
                dlq_entry = json.load(f)
                if isinstance(dlq_entry, list):
                    dlq_entry = dlq_entry[0]

            assert "item" in dlq_entry, "DLQ entry missing item"
            assert "function" in dlq_entry["item"], "DLQ entry missing function name"
            assert (
                "error" in dlq_entry or "retry_count" in dlq_entry
            ), "DLQ entry missing error metadata"

    def test_dlq_preserves_telemetry_payload(self):
        """Test that DLQ preserves full telemetry payload for recovery.

        Validates that enough context is saved to reconstruct and retry
        the failed telemetry export later.
        """
        from mca_sdk.buffering.retry import RetryPolicy
        from mca_sdk.resilience.dead_letter_queue import DeadLetterQueue

        with tempfile.TemporaryDirectory() as tmpdir:
            dlq_path = Path(tmpdir) / "dlq.json"

            dlq = DeadLetterQueue(queue_dir=str(dlq_path))
            retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01, dlq=dlq)

            # Function with complex payload
            test_payload = {
                "metric_name": "test.metric",
                "value": 42,
                "attributes": {"key": "value"},
            }

            def export_with_payload(payload):
                raise ConnectionError("Export failed")

            # Execute with retry
            with pytest.raises(ConnectionError):
                retry_policy.execute(export_with_payload, test_payload)

            # Verify DLQ preserved payload
            assert dlq_path.exists()
            dlq_files = [dlq_path]

            with open(dlq_files[0], "r") as f:
                dlq_entry = json.load(f)
                if isinstance(dlq_entry, list):
                    dlq_entry = dlq_entry[0]

            # Verify payload is preserved in args
            assert "item" in dlq_entry
            assert "args" in dlq_entry["item"], "DLQ entry missing function args"
            # Payload should be in args
            saved_args = dlq_entry["item"]["args"]
            assert len(saved_args) > 0, "DLQ should preserve function arguments"

    def test_dlq_handles_serialization_errors_gracefully(self):
        """Test that DLQ handles non-serializable payloads gracefully.

        Validates that DLQ doesn't crash when trying to persist payloads
        containing objects that can't be JSON serialized.
        """
        from mca_sdk.buffering.retry import RetryPolicy
        from mca_sdk.resilience.dead_letter_queue import DeadLetterQueue

        with tempfile.TemporaryDirectory() as tmpdir:
            dlq_path = Path(tmpdir) / "dlq.json"

            dlq = DeadLetterQueue(queue_dir=str(dlq_path))
            retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01, dlq=dlq)

            # Non-serializable object
            class NonSerializable:
                def __init__(self):
                    self.func = lambda x: x  # Lambdas can't be JSON serialized

            def export_non_serializable(obj):
                raise ConnectionError("Failed")

            # Should handle gracefully (either skip DLQ or use repr)
            try:
                retry_policy.execute(export_non_serializable, NonSerializable())
            except ConnectionError:
                pass  # Expected to fail after retries

            # DLQ should either have entry (with repr) or gracefully skip
            # Key is: should not crash the retry mechanism
            dlq_files = [dlq_path]
            # Either wrote placeholder or skipped - both are acceptable
            # The important thing is no crash occurred

    def test_dlq_respects_max_size_limit(self):
        """Test that DLQ respects maximum size configuration.

        Validates that DLQ doesn't grow unbounded, preventing disk exhaustion.
        """
        from mca_sdk.resilience.dead_letter_queue import DeadLetterQueue

        with tempfile.TemporaryDirectory() as tmpdir:
            dlq_path = Path(tmpdir) / "dlq.json"

            # Create DLQ with very small limit
            dlq = DeadLetterQueue(queue_dir=str(dlq_path), max_size_mb=1)  # 1 MB limit

            # Try to enqueue many large items
            large_item = {"data": "x" * 100000}  # ~100KB per item

            items_written = 0
            for i in range(20):  # Try to write 20 items (would exceed 1 MB)
                try:
                    dlq.enqueue(item=large_item, retry_count=3, error=Exception("Test"))
                    items_written += 1
                except Exception:
                    # DLQ should reject once limit reached
                    break

            # Verify DLQ enforced limit (shouldn't write all 20 items)
            dlq_files = [dlq_path]
            total_size = sum(f.stat().st_size for f in dlq_files)

            # Should respect ~1 MB limit (with some tolerance)
            assert (
                total_size < 2 * 1024 * 1024
            ), f"DLQ size {total_size} bytes exceeds max_size_mb limit"


class TestDLQRecovery:
    """Test DLQ recovery and replay functionality."""

    def test_dlq_items_can_be_read_and_replayed(self):
        """Test that persisted DLQ items can be read for retry/recovery.

        Validates that the DLQ format supports recovery operations.
        """
        from mca_sdk.resilience.dead_letter_queue import DeadLetterQueue

        with tempfile.TemporaryDirectory() as tmpdir:
            dlq_path = Path(tmpdir) / "dlq.json"

            dlq = DeadLetterQueue(queue_dir=str(dlq_path))

            # Write test item
            test_item = {
                "function": "export_metrics",
                "args": [{"metric": "test"}],
                "kwargs": {},
            }

            dlq.enqueue(item=test_item, retry_count=3, error=ConnectionError("Test"))

            # Verify item can be read
            assert dlq_path.exists()
            dlq_files = [dlq_path]

            with open(dlq_files[0], "r") as f:
                recovered_item = json.load(f)
                if isinstance(recovered_item, list):
                    recovered_item = recovered_item[0]

            # Verify structure supports recovery
            assert (
                "function" in recovered_item or "item" in recovered_item
            ), "DLQ entry should contain function name or item data for recovery"

    def test_dlq_provides_item_count_statistics(self):
        """Test that DLQ provides statistics for monitoring.

        Validates that operators can monitor DLQ size and health.
        """
        from mca_sdk.resilience.dead_letter_queue import DeadLetterQueue

        with tempfile.TemporaryDirectory() as tmpdir:
            dlq_path = Path(tmpdir) / "dlq.json"

            dlq = DeadLetterQueue(queue_dir=str(dlq_path))

            # Write some items
            for i in range(5):
                dlq.enqueue(item={"id": i}, retry_count=3, error=Exception(f"Error {i}"))

            # Get statistics
            if hasattr(dlq, "get_stats"):
                stats = dlq.get_stats()
                assert (
                    "item_count" in stats or "size" in stats
                ), "DLQ should provide statistics for monitoring"
            else:
                # Fallback: count files manually
                dlq_files = [dlq_path]
                dlq_entries = json.loads(dlq_files[0].read_text())
            assert len(dlq_entries) == 5, "Should have 5 DLQ items"


class TestRetryPolicyDLQIntegration:
    """Test integration between RetryPolicy and DLQ."""

    def test_retry_policy_only_writes_to_dlq_after_exhaustion(self):
        """Test that DLQ is only used after all retries are exhausted.

        Validates that we don't prematurely write to DLQ on first failure.
        """
        from mca_sdk.buffering.retry import RetryPolicy
        from mca_sdk.resilience.dead_letter_queue import DeadLetterQueue

        with tempfile.TemporaryDirectory() as tmpdir:
            dlq_path = Path(tmpdir) / "dlq.json"

            dlq = DeadLetterQueue(queue_dir=str(dlq_path))
            retry_policy = RetryPolicy(max_attempts=3, base_delay=0.01, dlq=dlq)

            attempt_count = 0

            def fail_twice_succeed_third():
                nonlocal attempt_count
                attempt_count += 1
                if attempt_count < 3:
                    raise ConnectionError("Temporary failure")
                return "success"

            # Should succeed on third attempt
            result = retry_policy.execute(fail_twice_succeed_third)
            assert result == "success"

            # Verify DLQ is empty (didn't write because eventually succeeded)
            dlq_files = [dlq_path]
            if dlq_path.exists():
                data = json.loads(dlq_path.read_text())
                assert len(data) == 0, "DLQ should be empty when retry eventually succeeds"
