import pytest
from unittest.mock import patch


@pytest.fixture(autouse=True)
def mock_registry_client():
    with patch("mca_sdk.registry.client.RegistryClient._fetch_model_config_internal") as mock_fetch:
        # Prevent TypeError: '>=' not supported between instances of 'MagicMock' and 'int'
        from mca_sdk.registry.models import ModelConfig

        mock_fetch.return_value = ModelConfig(
            model_id="test",
            model_version="1.0",
            service_name="test-service",
            team_name="test-team",
            model_category="internal",
            model_type="regression",
        )
        yield mock_fetch


"""Integration tests for RetryPolicy with TelemetryQueue and exporters.

Tests the integration between retry logic and queue buffering as specified
in Story 3.2 Task "Integrate RetryPolicy with TelemetryQueue".
"""

import time
import pytest
from unittest.mock import Mock, MagicMock
from mca_sdk.buffering.retry import RetryPolicy
from mca_sdk.buffering.queue import TelemetryQueue
from mca_sdk.buffering.queued_item import QueuedItem
from mca_sdk.buffering.exporters import BufferedOTLPMetricExporter
from opentelemetry.sdk.metrics.export import MetricExportResult


class TestRetryPolicyWithQueue:
    """Test RetryPolicy integration with TelemetryQueue."""

    def test_queue_integration_basic_flow(self):
        """Test basic flow: export fails -> queue -> retry -> success."""
        queue = TelemetryQueue(max_size=10)
        retry_policy = RetryPolicy(max_attempts=3, base_delay=0.01)

        # Simulate export exhausting all retries, then success on dequeue retry
        mock_export = Mock(
            side_effect=[
                Exception("temp failure"),
                Exception("temp failure"),
                Exception("temp failure"),
                "success",  # Succeeds after dequeue
            ]
        )

        def export_with_fallback(data):
            def fallback():
                queue.enqueue(data)
                return None

            return retry_policy.execute_with_fallback(mock_export, fallback, data)

        # First export fails, data queued
        result = export_with_fallback("test_data")
        assert result is None  # Fallback called
        assert queue.size() == 1
        assert mock_export.call_count == 3  # All retry attempts exhausted

        # Dequeue and retry succeeds
        queued_data = queue.dequeue()
        result = retry_policy.execute(mock_export, queued_data)
        assert result == "success"
        assert queue.size() == 0

    def test_queue_fallback_on_retry_exhaustion(self):
        """Test data queued after retry exhaustion (AC#2)."""
        queue = TelemetryQueue(max_size=100)
        retry_policy = RetryPolicy(max_attempts=3, base_delay=0.01)

        mock_export = Mock(side_effect=Exception("persistent failure"))

        def export_with_queue_fallback(data):
            def queue_fallback():
                queue.enqueue(data)
                return None

            return retry_policy.execute_with_fallback(mock_export, queue_fallback, data)

        # Export with fallback
        result = export_with_queue_fallback("telemetry_data")

        # Should have queued after 3 retry attempts
        assert result is None
        assert queue.size() == 1
        assert mock_export.call_count == 3
        assert queue.dequeue() == "telemetry_data"

    def test_queued_item_attempt_tracking(self):
        """Test QueuedItem tracks retry attempts across queue cycles."""
        queue = TelemetryQueue(max_size=10)
        retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01)

        # Create queued item with attempt counter
        item = QueuedItem(data="test_data", attempt_count=0)
        queue.enqueue(item)

        # First dequeue attempt fails
        dequeued = queue.dequeue()
        assert isinstance(dequeued, QueuedItem)
        assert dequeued.attempt_count == 0

        # Increment and re-queue
        dequeued.attempt_count += 1
        dequeued.last_error = "Export failed"
        queue.enqueue(dequeued)

        # Second dequeue attempt
        dequeued2 = queue.dequeue()
        assert dequeued2.attempt_count == 1
        assert dequeued2.last_error == "Export failed"

    def test_max_queue_retry_attempts_enforcement(self):
        """Test items dropped after max_queue_retry_attempts."""
        queue = TelemetryQueue(max_size=10)
        max_queue_retries = 3

        # Simulate queue retry cycle
        item = QueuedItem(data="persistent_failure", attempt_count=0)

        for attempt in range(max_queue_retries):
            item.attempt_count = attempt
            queue.enqueue(item)
            dequeued = queue.dequeue()
            # Simulate failed export
            dequeued.attempt_count += 1

        # After 3 attempts, item should be dropped (not re-queued)
        assert item.attempt_count >= max_queue_retries


class TestBufferedExporterIntegration:
    """Test RetryPolicy integration with BufferedOTLPMetricExporter."""

    def test_buffered_exporter_with_retry_policy(self):
        """Test BufferedOTLPMetricExporter uses RetryPolicy for exports."""
        retry_policy = RetryPolicy(max_attempts=3, base_delay=0.01)
        queue = TelemetryQueue(max_size=100)

        exporter = BufferedOTLPMetricExporter(
            endpoint="http://localhost:4318/v1/metrics",
            queue=queue,
            retry_policy=retry_policy,
            max_retry_queue_size=10,
        )

        # Mock the underlying exporter's export method
        mock_metrics_data = Mock()
        with patch.object(exporter._exporter, "export", return_value=MetricExportResult.SUCCESS):
            result = exporter.export(mock_metrics_data)

        assert result == MetricExportResult.SUCCESS
        assert exporter._export_attempts == 1
        assert exporter._export_successes == 1

    def test_buffered_exporter_queues_on_failure(self):
        """Test BufferedOTLPMetricExporter queues data on export failure."""
        retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01)
        queue = TelemetryQueue(max_size=100)

        exporter = BufferedOTLPMetricExporter(
            endpoint="http://localhost:4318/v1/metrics",
            queue=queue,
            retry_policy=retry_policy,
            max_retry_queue_size=10,
        )

        # Mock export to fail all retry attempts
        mock_metrics_data = Mock()
        with patch.object(
            exporter._exporter, "export", side_effect=Exception("collector unreachable")
        ):
            result = exporter.export(mock_metrics_data)

        # Returns SUCCESS because data was accepted (queued for later retry)
        # Buffered exporter semantics: SUCCESS = data accepted, FAILURE = data rejected
        assert result == MetricExportResult.SUCCESS
        assert queue.size() == 1  # Data was queued
        assert exporter._export_failures == 1

    def test_background_worker_drains_queue_with_retry(self):
        """Test background worker dequeues and retries with RetryPolicy."""
        retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01)
        queue = TelemetryQueue(max_size=100)

        exporter = BufferedOTLPMetricExporter(
            endpoint="http://localhost:4318/v1/metrics",
            queue=queue,
            retry_policy=retry_policy,
            drain_interval=0.1,  # Fast drain for test
            max_retry_queue_size=10,
        )

        # Manually enqueue an item
        item = QueuedItem(data=Mock(), attempt_count=0)
        queue.enqueue(item)
        assert queue.size() == 1

        # Start background worker
        exporter.start_background_worker()

        # Mock successful export
        with patch.object(exporter._exporter, "export", return_value=MetricExportResult.SUCCESS):
            # Wait for worker to drain queue
            time.sleep(0.3)

        # Queue should be drained
        assert queue.size() == 0

        # Stop worker
        exporter.stop_background_worker()

    def test_buffered_exporter_stats_tracking(self):
        """Test BufferedOTLPMetricExporter tracks statistics correctly."""
        retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01)
        exporter = BufferedOTLPMetricExporter(
            endpoint="http://localhost:4318/v1/metrics", retry_policy=retry_policy
        )

        # Successful export
        mock_data1 = Mock()
        with patch.object(exporter._exporter, "export", return_value=MetricExportResult.SUCCESS):
            exporter.export(mock_data1)

        # Failed export (queued)
        mock_data2 = Mock()
        with patch.object(exporter._exporter, "export", side_effect=Exception("fail")):
            exporter.export(mock_data2)

        stats = exporter.get_stats()
        assert stats["export_attempts"] == 2
        assert stats["export_successes"] == 1
        assert stats["export_failures"] == 1
        assert stats["items_queued"] == 1

    def test_dual_layer_retry_logic(self):
        """Test dual-layer retry: export-level (RetryPolicy) + queue-level (QueuedItem)."""
        retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01)
        queue = TelemetryQueue(max_size=100)
        max_queue_retries = 3

        exporter = BufferedOTLPMetricExporter(
            endpoint="http://localhost:4318/v1/metrics",
            queue=queue,
            retry_policy=retry_policy,
            max_retry_attempts=max_queue_retries,
            max_retry_queue_size=10,
        )

        # Track export attempts
        export_attempts = []

        def failing_export(data, **kwargs):
            export_attempts.append(1)
            raise Exception("persistent failure")

        mock_data = Mock()

        # First attempt: export fails, retried 2 times, then queued
        with patch.object(exporter._exporter, "export", side_effect=failing_export):
            result = exporter.export(mock_data)

        # Should have tried 2 times (RetryPolicy max_attempts)
        assert len(export_attempts) == 2
        assert queue.size() == 1

        # Verify QueuedItem was queued
        queued_item = queue.dequeue()
        assert isinstance(queued_item, QueuedItem)
        assert queued_item.attempt_count == 0  # Not yet dequeued for retry


class TestRetryWithBatchExport:
    """Test retry logic with batch export operations."""

    def test_batch_dequeue_with_retry(self):
        """Test dequeue_batch items are retried individually."""
        queue = TelemetryQueue(max_size=100)
        retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01)

        # Enqueue multiple items
        for i in range(5):
            item = QueuedItem(data=f"data-{i}", attempt_count=0)
            queue.enqueue(item)

        # Dequeue batch
        batch = queue.dequeue_batch(3)
        assert len(batch) == 3

        # Retry each item in batch
        success_count = 0
        mock_export = Mock(return_value="success")

        for item in batch:
            result = retry_policy.execute(mock_export, item.data)
            if result == "success":
                success_count += 1

        assert success_count == 3
        assert mock_export.call_count == 3

    def test_batch_export_respects_size_limits(self):
        """Test batch export respects configured batch size."""
        queue = TelemetryQueue(max_size=100)

        # Enqueue 20 items
        for i in range(20):
            queue.enqueue(QueuedItem(data=f"data-{i}", attempt_count=0))

        # Dequeue with batch size limit
        batch_size = 10
        batch = queue.dequeue_batch(batch_size)

        assert len(batch) == batch_size
        assert queue.size() == 10  # 10 remaining


class TestRetryPerformance:
    """Test retry logic performance characteristics."""

    def test_retry_overhead_less_than_5_percent(self):
        """Test retry overhead is <5% of total latency (NFR per Story 3.2)."""
        retry_policy = RetryPolicy(max_attempts=1)  # No retries to measure overhead

        # Mock export that takes 100ms
        def slow_export(data):
            time.sleep(0.1)
            return "success"

        iterations = 10
        start = time.time()
        for _ in range(iterations):
            retry_policy.execute(slow_export, "data")
        elapsed = time.time() - start

        # Expected time: 10 * 0.1s = 1.0s
        # Overhead should be < 5% = 1.05s
        assert elapsed < 1.05

    def test_concurrent_queue_retries(self):
        """Test concurrent retry operations don't corrupt queue state."""
        import threading

        queue = TelemetryQueue(max_size=100)
        retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01)
        results = []

        def worker(thread_id):
            # Enqueue data
            item = QueuedItem(data=f"data-{thread_id}", attempt_count=0)
            queue.enqueue(item)

            # Dequeue and retry
            dequeued = queue.dequeue()
            if dequeued:
                mock_export = Mock(return_value=f"success-{thread_id}")
                result = retry_policy.execute(mock_export, dequeued.data)
                results.append(result)

        # Run 20 threads concurrently
        threads = [threading.Thread(target=worker, args=(i,)) for i in range(20)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # All should succeed without corruption
        assert len(results) == 20


class TestRetryErrorScenarios:
    """Test retry behavior with various error scenarios."""

    def test_retry_different_exception_types(self):
        """Test retry handles different exception types."""
        retry_policy = RetryPolicy(max_attempts=3, base_delay=0.01)

        # Network timeout
        mock_func = Mock(side_effect=[TimeoutError("timeout"), "success"])
        result = retry_policy.execute(mock_func)
        assert result == "success"

        # Connection error
        mock_func2 = Mock(side_effect=[ConnectionError("connection refused"), "success"])
        result2 = retry_policy.execute(mock_func2)
        assert result2 == "success"

    def test_non_retryable_errors_still_retry(self):
        """Test all exceptions are retried (no discrimination in RetryPolicy)."""
        retry_policy = RetryPolicy(max_attempts=3, base_delay=0.01)

        # Authentication error (normally non-retryable, but RetryPolicy retries all)
        mock_func = Mock(side_effect=PermissionError("unauthorized"))

        with pytest.raises(PermissionError):
            retry_policy.execute(mock_func)

        # Verified it tried all attempts
        assert mock_func.call_count == 3

    def test_queue_full_during_fallback(self):
        """Test fallback behavior when queue is full."""
        queue = TelemetryQueue(max_size=2)
        retry_policy = RetryPolicy(max_attempts=2, base_delay=0.01)

        # Fill queue
        queue.enqueue("data1")
        queue.enqueue("data2")
        assert queue.size() == 2

        # Try to fallback with full queue
        mock_export = Mock(side_effect=Exception("fail"))

        def queue_fallback():
            success = queue.enqueue("data3")
            return None if success else "dropped"

        result = retry_policy.execute_with_fallback(mock_export, queue_fallback, "data3")

        # Fallback should indicate queue was full
        assert result == "dropped"
