"""Comprehensive unit tests for buffered OTLP exporters (buffering/exporters.py).

Tests cover all three exporter classes: BufferedOTLPMetricExporter,
BufferedOTLPLogExporter, BufferedOTLPSpanExporter. Covers init, export,
retry, queue overflow, background worker, flush, shutdown, persistence
metrics, and circuit breaker integration.
Target: >90% coverage for mca_sdk.buffering.exporters module.
"""

import time
import threading
from unittest.mock import Mock, MagicMock, patch, PropertyMock

import pytest
from pybreaker import CircuitBreakerError

from opentelemetry.sdk.metrics.export import MetricExportResult
from opentelemetry.sdk._logs._internal.export import LogRecordExportResult
from opentelemetry.sdk.trace.export import SpanExportResult

from mca_sdk.buffering.exporters import (
    BufferedOTLPMetricExporter,
    BufferedOTLPLogExporter,
    BufferedOTLPSpanExporter,
)
from mca_sdk.buffering.queue import TelemetryQueue
from mca_sdk.buffering.retry import RetryPolicy
from mca_sdk.buffering.queued_item import QueuedItem
from mca_sdk.utils.exceptions import BufferingError


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_retry_policy():
    policy = Mock(spec=RetryPolicy)
    policy.execute = Mock()
    return policy


@pytest.fixture
def metric_queue():
    return TelemetryQueue(max_size=100)


@pytest.fixture
def log_queue():
    return TelemetryQueue(max_size=100)


@pytest.fixture
def span_queue():
    return TelemetryQueue(max_size=100)


# ---------------------------------------------------------------------------
# BufferedOTLPMetricExporter Tests
# ---------------------------------------------------------------------------
class TestBufferedOTLPMetricExporter:
    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_init_defaults(self, MockOTLP, metric_queue, mock_retry_policy):
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        assert exporter._drain_interval == 1.0
        assert exporter._max_retry_queue_size == 100
        assert exporter._max_retry_attempts == 3

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_init_invalid_drain_interval(self, MockOTLP):
        with pytest.raises(BufferingError, match="drain_interval must be positive"):
            BufferedOTLPMetricExporter(drain_interval=0)

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_init_invalid_max_retry_queue_size(self, MockOTLP):
        with pytest.raises(BufferingError, match="max_retry_queue_size must be positive"):
            BufferedOTLPMetricExporter(max_retry_queue_size=0)

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_init_with_persistence(self, MockOTLP, mock_retry_policy):
        exporter = BufferedOTLPMetricExporter(
            retry_policy=mock_retry_policy,
            persist_queue=True,
            persist_path="/tmp/test_metrics.json",
        )
        assert exporter._queue._persist is True

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_init_persist_default_path(self, MockOTLP, mock_retry_policy):
        exporter = BufferedOTLPMetricExporter(
            retry_policy=mock_retry_policy,
            persist_queue=True,
        )
        assert "queue_metrics.json" in exporter._queue._persist_path
        exporter._queue.shutdown()

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_export_success(self, MockOTLP, metric_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = MetricExportResult.SUCCESS
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        mock_data = Mock()
        result = exporter.export(mock_data)
        assert result == MetricExportResult.SUCCESS
        assert exporter._export_successes == 1

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_export_failure_queues(self, MockOTLP, metric_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = MetricExportResult.FAILURE
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        mock_data = Mock()
        result = exporter.export(mock_data)
        # Returns SUCCESS because data was accepted (queued for retry)
        # Buffered exporter semantics: SUCCESS = data accepted, FAILURE = data rejected
        assert result == MetricExportResult.SUCCESS
        assert exporter._export_failures == 1
        assert metric_queue.size() == 1

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_export_exception_queues(self, MockOTLP, metric_queue, mock_retry_policy):
        mock_retry_policy.execute.side_effect = ConnectionError("timeout")
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        result = exporter.export(Mock())
        # Returns SUCCESS because data was accepted (queued for retry)
        assert result == MetricExportResult.SUCCESS
        assert exporter._export_failures == 1

    @pytest.mark.error_scenario
    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_graceful_degradation_on_exporter_failure(
        self, MockOTLP, metric_queue, mock_retry_policy, caplog
    ):
        """Test graceful degradation: telemetry dropped, warning logged, no exception propagated.

        Verifies AC5 requirement for explicit graceful degradation behavior:
        - Telemetry is queued (not lost immediately)
        - Warning is logged about the failure
        - No exception propagates to caller (returns SUCCESS)
        """
        import logging

        caplog.set_level(logging.WARNING)

        mock_retry_policy.execute.side_effect = ConnectionError("collector unreachable")
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)

        # Export should not raise exception
        mock_data = Mock()
        result = None
        try:
            result = exporter.export(mock_data)
        except Exception as e:
            pytest.fail(f"Graceful degradation failed - exception propagated: {e}")

        # Assertions for graceful degradation behavior
        assert (
            result == MetricExportResult.SUCCESS
        ), "Should return SUCCESS (data accepted to queue)"
        assert metric_queue.size() == 1, "Telemetry should be queued for retry"
        assert exporter._export_failures == 1, "Failure counter should increment"

        # Verify warning was logged (graceful degradation requirement)
        warning_found = any(
            "fail" in record.message.lower()
            or "error" in record.message.lower()
            or "exception" in record.message.lower()
            for record in caplog.records
            if record.levelno >= logging.WARNING
        )
        assert warning_found, "Graceful degradation should log a warning about the failure"

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_export_circuit_breaker_error(self, MockOTLP, metric_queue, mock_retry_policy):
        mock_retry_policy.execute.side_effect = CircuitBreakerError(Mock())
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        result = exporter.export(Mock())
        # Returns SUCCESS because data was accepted (queued for retry)
        assert result == MetricExportResult.SUCCESS
        assert exporter._export_failures == 1
        assert metric_queue.size() == 1

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_queue_for_retry_full(self, MockOTLP, mock_retry_policy):
        small_queue = TelemetryQueue(max_size=5)
        exporter = BufferedOTLPMetricExporter(
            queue=small_queue,
            retry_policy=mock_retry_policy,
            max_retry_queue_size=2,
        )
        # Fill queue past limit
        small_queue.enqueue("item1")
        small_queue.enqueue("item2")
        # Should drop instead of enqueue
        exporter._queue_for_retry(Mock())
        # Queue size should not increase past 2
        assert small_queue.size() == 2

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_queue_for_retry_queue_full_drop(self, MockOTLP, mock_retry_policy):
        """Test when queue.enqueue returns False (queue is full)."""
        q = TelemetryQueue(max_size=1)
        exporter = BufferedOTLPMetricExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            max_retry_queue_size=100,
        )
        q.enqueue("existing")
        # Queue is now at max, next enqueue returns False
        exporter._queue_for_retry(Mock())

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_start_stop_background_worker(self, MockOTLP, metric_queue, mock_retry_policy):
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        exporter.start_background_worker()
        assert exporter._running is True
        assert exporter._worker_thread.is_alive()

        # Double start should be no-op
        exporter.start_background_worker()

        exporter.stop_background_worker()
        assert exporter._running is False

        # Double stop should be no-op
        exporter.stop_background_worker()

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_worker_loop_processes_queue(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = MetricExportResult.SUCCESS
        exporter = BufferedOTLPMetricExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        # Add QueuedItem to queue
        qi = QueuedItem(data=Mock(), attempt_count=0)
        q.enqueue(qi)

        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

        assert q.size() == 0
        assert exporter._export_successes >= 1

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_worker_loop_drops_after_max_attempts(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        exporter = BufferedOTLPMetricExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
            max_retry_attempts=2,
        )
        # Add item with exhausted attempts
        qi = QueuedItem(data=Mock(), attempt_count=2)
        q.enqueue(qi)

        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

        assert exporter._items_dropped >= 1

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_worker_loop_requeues_on_failure(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = MetricExportResult.FAILURE
        exporter = BufferedOTLPMetricExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        qi = QueuedItem(data=Mock(), attempt_count=0)
        q.enqueue(qi)

        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

        assert exporter._items_requeued >= 1

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_worker_loop_requeues_on_exception(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.side_effect = ConnectionError("fail")
        exporter = BufferedOTLPMetricExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        qi = QueuedItem(data=Mock(), attempt_count=0)
        q.enqueue(qi)

        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

        assert exporter._items_requeued >= 1

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_worker_loop_raw_data_backward_compat(self, MockOTLP, mock_retry_policy):
        """Test worker handles raw data (not wrapped in QueuedItem)."""
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = MetricExportResult.SUCCESS
        exporter = BufferedOTLPMetricExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue(Mock())  # Raw data, not QueuedItem

        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_flush_success(self, MockOTLP, metric_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = MetricExportResult.SUCCESS
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        qi = QueuedItem(data=Mock(), attempt_count=0)
        metric_queue.enqueue(qi)

        result = exporter.flush(timeout_millis=5000)
        assert result is True
        assert metric_queue.size() == 0

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_flush_timeout(self, MockOTLP, mock_retry_policy):
        """Flush returns False when timeout is reached with items remaining."""
        q = TelemetryQueue(max_size=100)
        exporter = BufferedOTLPMetricExporter(queue=q, retry_policy=mock_retry_policy)
        for _ in range(5):
            q.enqueue(QueuedItem(data=Mock(), attempt_count=0))

        # Mock time so elapsed >= timeout_seconds immediately on first check
        with patch("mca_sdk.buffering.exporters.time") as mock_time:
            mock_time.time.side_effect = [0.0, 100.0]  # start=0, first check=100
            result = exporter.flush(timeout_millis=100)
        assert result is False
        assert q.size() > 0

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_flush_raw_data(self, MockOTLP, metric_queue, mock_retry_policy):
        """Test flush with raw data (backward compat)."""
        mock_retry_policy.execute.return_value = MetricExportResult.SUCCESS
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        metric_queue.enqueue(Mock())  # Raw, not QueuedItem
        result = exporter.flush(timeout_millis=5000)
        assert result is True

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_flush_failure_logged(self, MockOTLP, metric_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = MetricExportResult.FAILURE
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        metric_queue.enqueue(QueuedItem(data=Mock(), attempt_count=0))
        exporter.flush(timeout_millis=5000)

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_force_flush(self, MockOTLP, metric_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = MetricExportResult.SUCCESS
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        # Mock the underlying exporter's force_flush to return True
        exporter._exporter.force_flush.return_value = True
        result = exporter.force_flush()
        assert result is True

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_force_flush_exception(self, MockOTLP, metric_queue, mock_retry_policy):
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        # Mock flush to raise exception
        with patch.object(exporter, "flush", side_effect=Exception("oops")) as mock_flush:
            result = exporter.force_flush()
        # force_flush catches exception and returns False
        assert result is False
        mock_flush.assert_called_once()

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_shutdown(self, MockOTLP, metric_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = MetricExportResult.SUCCESS
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        exporter.start_background_worker()
        exporter.shutdown()
        assert exporter._running is False

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_get_stats(self, MockOTLP, metric_queue, mock_retry_policy):
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        stats = exporter.get_stats()
        assert stats["export_attempts"] == 0
        assert stats["export_successes"] == 0
        assert stats["queue_size"] == 0

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_set_meter_creates_counters(self, MockOTLP, metric_queue, mock_retry_policy):
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        mock_meter = Mock()
        mock_meter.create_counter.return_value = Mock()
        exporter.set_meter(mock_meter)
        assert exporter._meter is mock_meter
        assert exporter._persistence_writes_counter is not None

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_emit_persistence_metrics_no_meter(self, MockOTLP, metric_queue, mock_retry_policy):
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        exporter._emit_persistence_metrics()  # Should be no-op

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_emit_persistence_metrics_no_persist(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        exporter = BufferedOTLPMetricExporter(queue=q, retry_policy=mock_retry_policy)
        mock_meter = Mock()
        mock_meter.create_counter.return_value = Mock()
        exporter.set_meter(mock_meter)
        exporter._emit_persistence_metrics()  # No persistence stats

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_emit_persistence_metrics_with_deltas(self, MockOTLP, mock_retry_policy):
        """Test emit_persistence_metrics emits deltas when persistence stats change."""
        q = TelemetryQueue(max_size=100, persist=True, persist_path="/tmp/test_pm.json")
        exporter = BufferedOTLPMetricExporter(queue=q, retry_policy=mock_retry_policy)
        mock_meter = Mock()
        mock_counter = Mock()
        mock_meter.create_counter.return_value = mock_counter
        exporter.set_meter(mock_meter)
        # Patch buffer_stats to return persistence data with deltas
        with patch.object(
            q,
            "buffer_stats",
            return_value={
                "persistence": {
                    "writes_attempted": 5,
                    "writes_succeeded": 3,
                    "writes_failed": 1,
                    "disk_full_skipped": 0,
                    "corruptions_detected": 1,
                }
            },
        ):
            exporter._emit_persistence_metrics()
        # Counters should have been called for non-zero deltas
        assert mock_counter.add.call_count >= 1
        q.shutdown()

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_worker_drops_when_queue_full(self, MockOTLP, mock_retry_policy):
        """Worker drops items when re-queue fails due to full queue."""
        q = TelemetryQueue(max_size=2)
        mock_retry_policy.execute.return_value = MetricExportResult.FAILURE
        exporter = BufferedOTLPMetricExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
            max_retry_queue_size=1,
        )
        q.enqueue(QueuedItem(data=Mock(), attempt_count=0))
        q.enqueue(QueuedItem(data=Mock(), attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_flush_underlying_exporter_error(self, MockOTLP, metric_queue, mock_retry_policy):
        """Flush handles error from underlying exporter.force_flush."""
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        exporter._exporter.force_flush.side_effect = Exception("flush fail")
        result = exporter.flush(timeout_millis=5000)
        assert result is True  # queue is empty

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_shutdown_underlying_exporter_error(self, MockOTLP, metric_queue, mock_retry_policy):
        """Shutdown handles error from underlying exporter.shutdown."""
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        exporter._exporter.shutdown.side_effect = Exception("shutdown fail")
        exporter.shutdown()  # Should not raise

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_flush_queue_empty_break(self, MockOTLP, metric_queue, mock_retry_policy):
        """Flush handles dequeue returning None (queue empty mid-loop)."""
        exporter = BufferedOTLPMetricExporter(queue=metric_queue, retry_policy=mock_retry_policy)
        # Queue has 1 item, dequeue returns it then None
        metric_queue.enqueue(QueuedItem(data=Mock(), attempt_count=0))
        mock_retry_policy.execute.return_value = MetricExportResult.SUCCESS
        result = exporter.flush(timeout_millis=5000)
        assert result is True


# ---------------------------------------------------------------------------
# BufferedOTLPLogExporter Tests
# ---------------------------------------------------------------------------
class TestBufferedOTLPLogExporter:
    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_init_defaults(self, MockOTLP, log_queue, mock_retry_policy):
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        assert exporter._drain_interval == 1.0

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_init_invalid_drain_interval(self, MockOTLP):
        with pytest.raises(BufferingError, match="drain_interval must be positive"):
            BufferedOTLPLogExporter(drain_interval=-1)

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_init_invalid_max_retry_queue_size(self, MockOTLP):
        with pytest.raises(BufferingError, match="max_retry_queue_size must be positive"):
            BufferedOTLPLogExporter(max_retry_queue_size=0)

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_init_persist_default_path(self, MockOTLP, mock_retry_policy):
        exporter = BufferedOTLPLogExporter(
            retry_policy=mock_retry_policy,
            persist_queue=True,
        )
        assert "queue_logs.json" in exporter._queue._persist_path
        exporter._queue.shutdown()

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_export_success(self, MockOTLP, log_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = LogRecordExportResult.SUCCESS
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        result = exporter.export([Mock()])
        assert result == LogRecordExportResult.SUCCESS
        assert exporter._export_successes == 1

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_export_failure_queues(self, MockOTLP, log_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = LogRecordExportResult.FAILURE
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        result = exporter.export([Mock()])
        assert result == LogRecordExportResult.SUCCESS  # data accepted (queued for retry)
        assert exporter._export_failures == 1

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_export_circuit_breaker_error(self, MockOTLP, log_queue, mock_retry_policy):
        mock_retry_policy.execute.side_effect = CircuitBreakerError(Mock())
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        result = exporter.export([Mock()])
        assert result == LogRecordExportResult.SUCCESS  # data accepted (queued for retry)

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_export_exception(self, MockOTLP, log_queue, mock_retry_policy):
        mock_retry_policy.execute.side_effect = ConnectionError("fail")
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        result = exporter.export([Mock()])
        assert result == LogRecordExportResult.SUCCESS  # data accepted (queued for retry)

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_queue_for_retry_full(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=5)
        exporter = BufferedOTLPLogExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            max_retry_queue_size=1,
        )
        q.enqueue("item1")
        exporter._queue_for_retry([Mock()])
        assert q.size() == 1

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_queue_for_retry_enqueue_false(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=1)
        exporter = BufferedOTLPLogExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            max_retry_queue_size=100,
        )
        q.enqueue("existing")
        exporter._queue_for_retry([Mock()])

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_start_stop_worker(self, MockOTLP, log_queue, mock_retry_policy):
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        exporter.start_background_worker()
        assert exporter._running is True
        exporter.start_background_worker()  # Double start
        exporter.stop_background_worker()
        assert exporter._running is False
        exporter.stop_background_worker()  # Double stop

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_worker_processes_queue(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = LogRecordExportResult.SUCCESS
        exporter = BufferedOTLPLogExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()
        assert exporter._export_successes >= 1

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_worker_drops_after_max_attempts(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        exporter = BufferedOTLPLogExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
            max_retry_attempts=1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=1))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()
        assert exporter._items_dropped >= 1

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_worker_requeues_on_failure(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = LogRecordExportResult.FAILURE
        exporter = BufferedOTLPLogExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()
        assert exporter._items_requeued >= 1

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_worker_requeues_on_exception(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.side_effect = ConnectionError("fail")
        exporter = BufferedOTLPLogExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()
        assert exporter._items_requeued >= 1

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_worker_raw_data_compat(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = LogRecordExportResult.SUCCESS
        exporter = BufferedOTLPLogExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue([Mock()])  # Raw data
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_flush_success(self, MockOTLP, log_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = LogRecordExportResult.SUCCESS
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        log_queue.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        result = exporter.flush(timeout_millis=5000)
        assert result is True

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_flush_raw_data(self, MockOTLP, log_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = LogRecordExportResult.SUCCESS
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        log_queue.enqueue([Mock()])
        result = exporter.flush(timeout_millis=5000)
        assert result is True

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_flush_failure(self, MockOTLP, log_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = LogRecordExportResult.FAILURE
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        log_queue.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.flush(timeout_millis=5000)

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_flush_timeout(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        exporter = BufferedOTLPLogExporter(queue=q, retry_policy=mock_retry_policy)
        for _ in range(5):
            q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        with patch("mca_sdk.buffering.exporters.time") as mock_time:
            mock_time.time.side_effect = [0.0, 100.0]
            result = exporter.flush(timeout_millis=100)
        assert result is False

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_shutdown(self, MockOTLP, log_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = LogRecordExportResult.SUCCESS
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        exporter.start_background_worker()
        exporter.shutdown()
        assert exporter._running is False

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_get_stats(self, MockOTLP, log_queue, mock_retry_policy):
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        stats = exporter.get_stats()
        assert "export_attempts" in stats
        assert stats["queue_size"] == 0

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_set_meter(self, MockOTLP, log_queue, mock_retry_policy):
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        mock_meter = Mock()
        mock_meter.create_counter.return_value = Mock()
        exporter.set_meter(mock_meter)
        assert exporter._meter is mock_meter

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_emit_persistence_metrics_no_meter(self, MockOTLP, log_queue, mock_retry_policy):
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        exporter._emit_persistence_metrics()  # no-op

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_emit_persistence_metrics_with_deltas(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100, persist=True, persist_path="/tmp/test_lpm.json")
        exporter = BufferedOTLPLogExporter(queue=q, retry_policy=mock_retry_policy)
        mock_meter = Mock()
        mock_counter = Mock()
        mock_meter.create_counter.return_value = mock_counter
        exporter.set_meter(mock_meter)
        with patch.object(
            q,
            "buffer_stats",
            return_value={
                "persistence": {
                    "writes_attempted": 3,
                    "writes_succeeded": 2,
                    "writes_failed": 1,
                    "disk_full_skipped": 0,
                    "corruptions_detected": 0,
                }
            },
        ):
            exporter._emit_persistence_metrics()
        assert mock_counter.add.call_count >= 1
        q.shutdown()

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_worker_drops_when_queue_full(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=2)
        mock_retry_policy.execute.return_value = LogRecordExportResult.FAILURE
        exporter = BufferedOTLPLogExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
            max_retry_queue_size=1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_flush_underlying_exporter_error(self, MockOTLP, log_queue, mock_retry_policy):
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        exporter._exporter.force_flush.side_effect = Exception("flush fail")
        result = exporter.flush(timeout_millis=5000)
        assert result is True

    @patch("mca_sdk.buffering.exporters.OTLPLogExporter")
    def test_shutdown_underlying_exporter_error(self, MockOTLP, log_queue, mock_retry_policy):
        exporter = BufferedOTLPLogExporter(queue=log_queue, retry_policy=mock_retry_policy)
        exporter._exporter.shutdown.side_effect = Exception("shutdown fail")
        exporter.shutdown()


# ---------------------------------------------------------------------------
# BufferedOTLPSpanExporter Tests
# ---------------------------------------------------------------------------
class TestBufferedOTLPSpanExporter:
    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_init_defaults(self, MockOTLP, span_queue, mock_retry_policy):
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        assert exporter._drain_interval == 1.0

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_init_invalid_drain_interval(self, MockOTLP):
        with pytest.raises(BufferingError):
            BufferedOTLPSpanExporter(drain_interval=0)

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_init_invalid_max_retry(self, MockOTLP):
        with pytest.raises(BufferingError):
            BufferedOTLPSpanExporter(max_retry_queue_size=-1)

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_init_persist_default_path(self, MockOTLP, mock_retry_policy):
        exporter = BufferedOTLPSpanExporter(
            retry_policy=mock_retry_policy,
            persist_queue=True,
        )
        assert "queue_traces.json" in exporter._queue._persist_path
        exporter._queue.shutdown()

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_export_success(self, MockOTLP, span_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = SpanExportResult.SUCCESS
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        result = exporter.export([Mock()])
        assert result == SpanExportResult.SUCCESS

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_export_failure_queues(self, MockOTLP, span_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = SpanExportResult.FAILURE
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        result = exporter.export([Mock()])
        assert result == SpanExportResult.SUCCESS  # data accepted (queued for retry)
        assert span_queue.size() == 1

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_export_circuit_breaker(self, MockOTLP, span_queue, mock_retry_policy):
        mock_retry_policy.execute.side_effect = CircuitBreakerError(Mock())
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        result = exporter.export([Mock()])
        assert result == SpanExportResult.SUCCESS  # data accepted (queued for retry)

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_export_exception(self, MockOTLP, span_queue, mock_retry_policy):
        mock_retry_policy.execute.side_effect = RuntimeError("fail")
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        result = exporter.export([Mock()])
        assert result == SpanExportResult.SUCCESS  # data accepted (queued for retry)

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_queue_for_retry_full(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=5)
        exporter = BufferedOTLPSpanExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            max_retry_queue_size=1,
        )
        q.enqueue("item")
        exporter._queue_for_retry([Mock()])

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_queue_for_retry_enqueue_false(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=1)
        exporter = BufferedOTLPSpanExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            max_retry_queue_size=100,
        )
        q.enqueue("existing")
        exporter._queue_for_retry([Mock()])

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_start_stop_worker(self, MockOTLP, span_queue, mock_retry_policy):
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        exporter.start_background_worker()
        exporter.start_background_worker()  # Double start
        exporter.stop_background_worker()
        exporter.stop_background_worker()  # Double stop

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_worker_processes_queue(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = SpanExportResult.SUCCESS
        exporter = BufferedOTLPSpanExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()
        assert exporter._export_successes >= 1

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_worker_drops_after_max_attempts(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        exporter = BufferedOTLPSpanExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
            max_retry_attempts=1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=1))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()
        assert exporter._items_dropped >= 1

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_worker_requeues_on_failure(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = SpanExportResult.FAILURE
        exporter = BufferedOTLPSpanExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()
        assert exporter._items_requeued >= 1

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_worker_requeues_on_exception(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.side_effect = RuntimeError("fail")
        exporter = BufferedOTLPSpanExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()
        assert exporter._items_requeued >= 1

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_worker_raw_data_compat(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        mock_retry_policy.execute.return_value = SpanExportResult.SUCCESS
        exporter = BufferedOTLPSpanExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
        )
        q.enqueue([Mock()])
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_flush_success(self, MockOTLP, span_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = SpanExportResult.SUCCESS
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        span_queue.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        result = exporter.flush(timeout_millis=5000)
        assert result is True

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_flush_raw_data(self, MockOTLP, span_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = SpanExportResult.SUCCESS
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        span_queue.enqueue([Mock()])
        result = exporter.flush(timeout_millis=5000)
        assert result is True

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_flush_failure(self, MockOTLP, span_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = SpanExportResult.FAILURE
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        span_queue.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.flush(timeout_millis=5000)

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_flush_timeout(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100)
        exporter = BufferedOTLPSpanExporter(queue=q, retry_policy=mock_retry_policy)
        for _ in range(5):
            q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        with patch("mca_sdk.buffering.exporters.time") as mock_time:
            mock_time.time.side_effect = [0.0, 100.0]
            result = exporter.flush(timeout_millis=100)
        assert result is False

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_shutdown(self, MockOTLP, span_queue, mock_retry_policy):
        mock_retry_policy.execute.return_value = SpanExportResult.SUCCESS
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        exporter.start_background_worker()
        exporter.shutdown()

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_get_stats(self, MockOTLP, span_queue, mock_retry_policy):
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        stats = exporter.get_stats()
        assert "export_attempts" in stats

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_set_meter(self, MockOTLP, span_queue, mock_retry_policy):
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        mock_meter = Mock()
        mock_meter.create_counter.return_value = Mock()
        exporter.set_meter(mock_meter)
        assert exporter._meter is mock_meter

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_emit_persistence_metrics_no_meter(self, MockOTLP, span_queue, mock_retry_policy):
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        exporter._emit_persistence_metrics()  # no-op

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_emit_persistence_metrics_with_deltas(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=100, persist=True, persist_path="/tmp/test_spm.json")
        exporter = BufferedOTLPSpanExporter(queue=q, retry_policy=mock_retry_policy)
        mock_meter = Mock()
        mock_counter = Mock()
        mock_meter.create_counter.return_value = mock_counter
        exporter.set_meter(mock_meter)
        with patch.object(
            q,
            "buffer_stats",
            return_value={
                "persistence": {
                    "writes_attempted": 2,
                    "writes_succeeded": 2,
                    "writes_failed": 0,
                    "disk_full_skipped": 1,
                    "corruptions_detected": 0,
                }
            },
        ):
            exporter._emit_persistence_metrics()
        assert mock_counter.add.call_count >= 1
        q.shutdown()

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_worker_drops_when_queue_full(self, MockOTLP, mock_retry_policy):
        q = TelemetryQueue(max_size=2)
        mock_retry_policy.execute.return_value = SpanExportResult.FAILURE
        exporter = BufferedOTLPSpanExporter(
            queue=q,
            retry_policy=mock_retry_policy,
            drain_interval=0.1,
            max_retry_queue_size=1,
        )
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        q.enqueue(QueuedItem(data=[Mock()], attempt_count=0))
        exporter.start_background_worker()
        time.sleep(0.5)
        exporter.stop_background_worker()

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_flush_underlying_exporter_error(self, MockOTLP, span_queue, mock_retry_policy):
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        exporter._exporter.force_flush.side_effect = Exception("flush fail")
        result = exporter.flush(timeout_millis=5000)
        assert result is True

    @patch("mca_sdk.buffering.exporters.OTLPSpanExporter")
    def test_shutdown_underlying_exporter_error(self, MockOTLP, span_queue, mock_retry_policy):
        exporter = BufferedOTLPSpanExporter(queue=span_queue, retry_policy=mock_retry_policy)
        exporter._exporter.shutdown.side_effect = Exception("shutdown fail")
        exporter.shutdown()
