from .performance_helpers import get_collector_endpoint

"""Backpressure and buffering resilience tests for MCA SDK.

This module validates that the SDK correctly handles backpressure scenarios
when the collector is slow, intermittent, or offline.

Acceptance Criteria (AC 3):
- SDK respects max_queue_size limit
- Drops items according to strategy when queue is full
- No MemoryError or OutOfMemoryError occurs
- Application is never blocked by full queues (non-blocking enqueues)
"""

import logging
import threading
import time
from typing import Dict, List
from unittest.mock import Mock, patch

import pytest
from opentelemetry.sdk.metrics.export import MetricExportResult

from mca_sdk import MCAClient
from mca_sdk.buffering.queue import TelemetryQueue

# Mark as performance test
pytestmark = pytest.mark.performance

logger = logging.getLogger(__name__)


class MockSlowCollector:
    """Mock collector that simulates network latency."""

    def __init__(self, *args, **kwargs):
        """Initialize mock slow collector."""
        self.latency_seconds = 0.1  # 100ms latency is enough to fill queue with 500 items/s
        self.requests_received = 0
        self.lock = threading.Lock()
        # Required by PeriodicExportingMetricReader
        self._preferred_temporality = {}
        self._preferred_aggregation = {}

    def export(self, metrics_data, **kwargs):
        """Simulate slow export with latency."""
        time.sleep(self.latency_seconds)
        with self.lock:
            self.requests_received += 1
        return MetricExportResult.SUCCESS

    def shutdown(self, **kwargs):
        pass

    def force_flush(self, **kwargs):
        return True


class MockFailingCollector:
    """Mock collector that fails intermittently."""

    def __init__(self, failure_rate: float = 0.5):
        """Initialize mock failing collector."""
        self.failure_rate = failure_rate
        self.requests_received = 0
        self.failures = 0
        self.lock = threading.Lock()

    def export(self, items):
        """Simulate intermittent failures."""
        import random

        with self.lock:
            self.requests_received += 1
            if random.random() < self.failure_rate:
                self.failures += 1
                raise ConnectionError("Simulated collector failure")
        return True


class MockOfflineCollector:
    """Mock collector that is completely offline."""

    def __init__(self):
        """Initialize mock offline collector."""
        self.requests_attempted = 0

    def export(self, items):
        """Always fail (simulate offline)."""
        self.requests_attempted += 1
        raise ConnectionError("Collector offline")


def test_queue_respects_max_size():
    """Validate queue respects max_queue_size limit.

    Acceptance Criteria:
    - AC 3.1: Queue size never exceeds max_queue_size
    """
    max_size = 100
    queue = TelemetryQueue(max_size=max_size)

    # Fill queue beyond capacity
    for i in range(200):
        queue.enqueue({"item": i})

    # Queue size should not exceed max_size
    assert queue.size() <= max_size, f"Queue size {queue.size()} exceeds max_size {max_size}"

    # Verify it's at max capacity
    assert queue.size() == max_size, f"Queue size {queue.size()} should be at max_size {max_size}"

    logger.info(f"Queue respects max_size: PASS (size={queue.size()}, max={max_size})")


def test_queue_drops_oldest_when_full():
    """Validate queue drops oldest items (FIFO) when full.

    Acceptance Criteria:
    - AC 3.2: SDK drops items according to strategy (default: drop oldest)
    """
    max_size = 10
    queue = TelemetryQueue(max_size=max_size)

    # Fill queue
    for i in range(max_size):
        queue.enqueue({"item": i, "value": f"original_{i}"})

    # Add more items (should evict oldest)
    for i in range(5):
        queue.enqueue({"item": max_size + i, "value": f"new_{i}"})

    # Dequeue all items
    items = []
    while not queue.is_empty():
        item = queue.dequeue()
        if item:
            items.append(item)

    # Expected behavior:
    # Initial queue: [0, 1, 2, ..., 9]
    # Add 10, 11, 12, 13, 14
    # Queue drops 0, 1, 2, 3, 4
    # Queue becomes: [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

    # First items in the dequeued list should be the ones that survived (start from 5)
    assert len(items) == max_size, f"Expected {max_size} items, got {len(items)}"

    # Items 0-4 should have been dropped
    item_ids = [item["item"] for item in items]
    assert 0 not in item_ids, "Item 0 should have been dropped"
    assert 4 not in item_ids, "Item 4 should have been dropped"

    # Items 5-9 should still be there
    assert 5 in item_ids, "Item 5 should be retained"
    assert 9 in item_ids, "Item 9 should be retained"

    # New items should be there
    assert max_size in item_ids, "New item 10 should be in queue"

    logger.info("Queue drops oldest items correctly: PASS")


def test_queue_non_blocking_enqueue():
    """Validate queue enqueue is non-blocking.

    Acceptance Criteria:
    - AC 3.3: Application is never blocked by full queues (non-blocking enqueues)
    """
    max_size = 100
    queue = TelemetryQueue(max_size=max_size)

    # Fill queue to capacity
    for i in range(max_size):
        queue.enqueue({"item": i})

    # Time enqueue operations on full queue
    enqueue_times = []
    for i in range(100):
        start = time.perf_counter()
        result = queue.enqueue({"item": max_size + i})
        elapsed = time.perf_counter() - start
        enqueue_times.append(elapsed)

    # All enqueues should be non-blocking (sub-millisecond)
    max_enqueue_time = max(enqueue_times)
    avg_enqueue_time = sum(enqueue_times) / len(enqueue_times)

    assert (
        max_enqueue_time < 0.001
    ), f"Enqueue blocked for {max_enqueue_time * 1000:.2f}ms (should be <1ms)"

    logger.info(
        f"Enqueue is non-blocking: PASS (max={max_enqueue_time * 1000:.3f}ms, avg={avg_enqueue_time * 1000:.3f}ms)"
    )


@patch("mca_sdk.buffering.exporters.OTLPMetricExporter", side_effect=MockSlowCollector)
def test_sdk_handles_slow_collector(mock_exporter):
    """Validate SDK handles slow collector without blocking application.

    Acceptance Criteria:
    - AC 3: SDK handles backpressure when collector is slow
    """
    # Create client with small queue
    client = MCAClient(
        service_name="backpressure_test",
        model_id="test_model",
        team_name="test_team",
        model_category="internal",
        model_type="regression",
        collector_endpoint=get_collector_endpoint(),
        buffering_enabled=True,
        max_queue_size=50,
    )

    # Rapid predictions (should fill queue quickly)
    start_time = time.time()
    for i in range(500):
        client.record_prediction(latency=0.001)

    elapsed_time = time.time() - start_time

    # Recording should not block significantly
    # 500 predictions should take <100ms with non-blocking enqueue
    assert elapsed_time < 0.5, (
        f"SDK blocked application for {elapsed_time:.2f}s with slow collector "
        f"(should be <0.5s for non-blocking enqueue)"
    )

    logger.info(
        f"SDK handles slow collector without blocking: PASS ({elapsed_time:.3f}s for 500 predictions)"
    )

    client.shutdown()


def test_sdk_handles_offline_collector_no_memory_error():
    """Validate SDK handles offline collector without MemoryError.

    Acceptance Criteria:
    - AC 3.4: No MemoryError occurs when collector is offline
    """
    # Create client with small queue
    client = MCAClient(
        service_name="offline_test",
        model_id="test_model",
        team_name="test_team",
        model_category="internal",
        model_type="regression",
        collector_endpoint="http://localhost:9999",  # Non-existent endpoint
        buffering_enabled=True,
        max_queue_size=100,
    )

    # Push many items (well beyond queue capacity)
    try:
        for i in range(10000):
            client.record_prediction(latency=0.001)

        # Should not raise MemoryError
        logger.info("SDK handles offline collector without MemoryError: PASS")

    except MemoryError as e:
        pytest.fail(f"MemoryError raised with offline collector: {e}")

    finally:
        client.shutdown()


def test_backpressure_full_scenario():
    """Full backpressure scenario: small queue, high volume, slow collector.

    Acceptance Criteria:
    - AC 3: Complete backpressure resilience validation
    """
    max_queue_size = 100
    items_to_push = 1000

    # Create queue
    queue = TelemetryQueue(max_size=max_queue_size)

    # Push items rapidly
    pushed_count = 0
    dropped_count = 0

    start_time = time.time()
    for i in range(items_to_push):
        # enqueue() returns False when queue was full (meaning oldest item was evicted)
        # Returns True when queue had space (no eviction occurred)
        result = queue.enqueue({"item": i})
        pushed_count += 1
        if not result:  # False means an eviction occurred (oldest item dropped)
            dropped_count += 1

    elapsed_time = time.time() - start_time

    # Verify results
    final_queue_size = queue.size()

    logger.info("\nBackpressure Scenario Results:")
    logger.info(f"  Items pushed:     {pushed_count}")
    logger.info(f"  Items dropped:    {dropped_count}")
    logger.info(f"  Final queue size: {final_queue_size}")
    logger.info(f"  Max queue size:   {max_queue_size}")
    logger.info(f"  Time elapsed:     {elapsed_time:.3f}s")

    # Assertions
    assert (
        final_queue_size <= max_queue_size
    ), f"Queue size {final_queue_size} exceeds max {max_queue_size}"

    expected_drops = max(0, items_to_push - max_queue_size)
    assert (
        dropped_count >= expected_drops - 10
    ), f"Expected ~{expected_drops} drops, got {dropped_count}"  # Allow small margin

    # Should not block significantly
    assert elapsed_time < 1.0, f"Backpressure scenario took {elapsed_time:.2f}s (should be <1s)"

    logger.info("Full backpressure scenario: PASS")


def test_buffer_stats_tracking():
    """Validate buffer statistics are tracked correctly.

    Tests buffer_stats() method during backpressure scenarios.
    """
    max_size = 100
    queue = TelemetryQueue(max_size=max_size)

    # Get initial stats
    stats = queue.buffer_stats()
    assert stats["size"] == 0
    assert stats["max_size"] == max_size
    assert stats["is_empty"]
    assert not stats["is_full"]
    assert stats["utilization"] == 0.0

    # Fill to 50%
    for i in range(50):
        queue.enqueue({"item": i})

    stats = queue.buffer_stats()
    assert stats["size"] == 50
    assert stats["utilization"] == 50.0
    assert not stats["is_empty"]
    assert not stats["is_full"]

    # Fill to 100%
    for i in range(50, 150):
        queue.enqueue({"item": i})

    stats = queue.buffer_stats()
    assert stats["size"] == max_size
    assert stats["utilization"] == 100.0
    assert stats["is_full"]

    logger.info("Buffer stats tracking: PASS")


if __name__ == "__main__":
    # Configure logging
    logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

    # Run tests
    test_queue_respects_max_size()
    test_queue_drops_oldest_when_full()
    test_queue_non_blocking_enqueue()
    # Note: Mock is not applied when running main unless we explicitly patch here
    # Use pytest to run tests

    logger.info("\n" + "=" * 60)
    logger.info("Use 'pytest tests/performance/test_backpressure.py' to run tests")
    logger.info("=" * 60)
