"""Integration tests for Circuit Breaker pattern.

Tests circuit breaker integration with MCAClient, exporters, and retry policies.
"""

import pytest
import time
from unittest.mock import Mock, patch, MagicMock
from pybreaker import CircuitBreakerError, STATE_OPEN, STATE_CLOSED, STATE_HALF_OPEN

from mca_sdk import MCAClient, MCAConfig
from mca_sdk.resilience.circuit_breaker import CircuitBreakerRetryPolicy
from mca_sdk.buffering.retry import RetryPolicy


class TestCircuitBreakerWithMCAClient:
    """Test circuit breaker integration with MCAClient."""

    def test_circuit_breaker_enabled_by_default(self):
        """Test circuit breaker is enabled by default in MCAClient when buffering is enabled."""
        config = MCAConfig(
            service_name="test-service",
            model_category="internal",
            model_type="regression",
            model_id="test-model",
            team_name="test-team",
            buffering_enabled=True,
        )
        client = MCAClient(config=config)

        assert client.config.circuit_breaker_enabled is True
        assert client._circuit_breaker_retry_policy is not None
        assert isinstance(client._circuit_breaker_retry_policy, CircuitBreakerRetryPolicy)

    def test_circuit_breaker_can_be_disabled(self):
        """Test circuit breaker can be disabled via config."""
        config = MCAConfig(
            service_name="test-service",
            model_category="internal",
            model_type="regression",
            model_id="test-model",
            team_name="test-team",
            circuit_breaker_enabled=False,
        )
        client = MCAClient(config=config)

        assert client.config.circuit_breaker_enabled is False
        assert client._circuit_breaker_retry_policy is None

    def test_circuit_breaker_custom_parameters(self):
        """Test custom circuit breaker parameters are used."""
        config = MCAConfig(
            service_name="test-service",
            model_category="internal",
            model_type="regression",
            model_id="test-model",
            team_name="test-team",
            buffering_enabled=True,
            circuit_breaker_fail_max=10,
            circuit_breaker_timeout=120,
        )
        client = MCAClient(config=config)

        cb = client._circuit_breaker_retry_policy
        assert cb is not None
        assert cb._circuit.fail_max == 10
        assert cb._circuit.reset_timeout == 120

    def test_buffer_stats_includes_circuit_breaker_info(self):
        """Test buffer_stats() includes circuit breaker information."""
        config = MCAConfig(
            service_name="test-service",
            model_category="internal",
            model_type="regression",
            model_id="test-model",
            team_name="test-team",
            buffering_enabled=True,
        )
        client = MCAClient(config=config)

        stats = client.buffer_stats()

        assert "circuit_breaker" in stats
        cb_stats = stats["circuit_breaker"]
        assert "state" in cb_stats
        assert "failure_count" in cb_stats
        assert cb_stats["state"] == "closed"


class TestCircuitBreakerWithRetryPolicy:
    """Test circuit breaker interaction with retry policy."""

    def test_retry_policy_wrapped_by_circuit_breaker(self):
        """Test retry policy is properly wrapped by circuit breaker."""
        retry_policy = RetryPolicy(max_attempts=3, base_delay=0.01)
        cb_retry = CircuitBreakerRetryPolicy(retry_policy=retry_policy, fail_max=2, reset_timeout=1)

        success_func = Mock(return_value="success")
        result = cb_retry.execute(success_func)

        assert result == "success"
        success_func.assert_called_once()

    def test_circuit_opens_after_repeated_failures(self):
        """Test circuit opens after max failures during retry attempts."""
        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(retry_policy=retry_policy, fail_max=3, reset_timeout=1)

        failing_func = Mock(side_effect=Exception("Export failed"))

        # Trigger failures to open circuit
        for _ in range(3):
            with pytest.raises(Exception):
                cb_retry.execute(failing_func)

        # Circuit should now be open
        stats = cb_retry.get_stats()
        assert stats["state"] == "open"

        # Next call should fast-fail
        with pytest.raises(CircuitBreakerError):
            cb_retry.execute(failing_func)

    def test_circuit_recovers_after_timeout(self):
        """Test circuit transitions to half-open after timeout."""
        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(
            retry_policy=retry_policy,
            fail_max=2,
            reset_timeout=0.1,  # Very short timeout
        )

        failing_func = Mock(side_effect=Exception("Export failed"))
        success_func = Mock(return_value="success")

        # Open the circuit
        for _ in range(2):
            with pytest.raises(Exception):
                cb_retry.execute(failing_func)

        assert cb_retry.get_state() == "open"

        # Wait for timeout
        time.sleep(0.15)

        # Next successful call should transition to half-open then closed
        result = cb_retry.execute(success_func)
        assert result == "success"
        assert cb_retry.get_state() == "closed"


class TestCircuitBreakerWithExporters:
    """Test circuit breaker integration with OTLP exporters."""

    def test_exporter_handles_circuit_breaker_error(self):
        """Test exporters handle CircuitBreakerError gracefully."""
        from mca_sdk.buffering.exporters import BufferedOTLPMetricExporter
        from opentelemetry.sdk.metrics.export import MetricsData

        # Create exporter with circuit breaker
        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(retry_policy=retry_policy, fail_max=1, reset_timeout=1)

        # Open circuit by causing failure
        with pytest.raises(Exception):
            cb_retry.execute(Mock(side_effect=Exception("Failed")))

        assert cb_retry.get_state() == "open"

        # Exporter should handle CircuitBreakerError
        exporter = BufferedOTLPMetricExporter(
            endpoint="http://localhost:4318", retry_policy=cb_retry
        )

        # Create mock metrics data
        mock_data = Mock(spec=MetricsData)

        # Should return SUCCESS (data was accepted and queued for retry)
        from opentelemetry.sdk.metrics.export import MetricExportResult

        result = exporter.export(mock_data)

        # Exporter returns SUCCESS when circuit is open because data was queued
        # This follows OTel semantics: SUCCESS = data accepted, FAILURE = data lost
        # Circuit breaker prevents immediate export but data is safely queued
        assert result == MetricExportResult.SUCCESS
        # Verify data was queued
        assert exporter._queue.size() > 0

    @patch("mca_sdk.buffering.exporters.OTLPMetricExporter")
    def test_metrics_exporter_integrates_with_circuit_breaker(self, mock_otlp):
        """Test BufferedOTLPMetricExporter works with circuit breaker."""
        from mca_sdk.buffering.exporters import BufferedOTLPMetricExporter
        from opentelemetry.sdk.metrics.export import MetricsData, MetricExportResult

        # Mock OTLP exporter to succeed
        mock_instance = mock_otlp.return_value
        mock_instance.export.return_value = MetricExportResult.SUCCESS

        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(retry_policy=retry_policy, fail_max=2, reset_timeout=1)

        exporter = BufferedOTLPMetricExporter(
            endpoint="http://localhost:4318", retry_policy=cb_retry
        )

        mock_data = Mock(spec=MetricsData)

        # Exporter should work normally with circuit breaker
        result = exporter.export(mock_data)
        assert result == MetricExportResult.SUCCESS

        # Circuit should remain closed
        stats = cb_retry.get_stats()
        assert stats["state"] == "closed"


class TestCircuitBreakerStateTransitions:
    """Test circuit breaker state machine in integration scenarios."""

    def test_full_state_cycle_closed_open_halfopen_closed(self):
        """Test complete state transition cycle."""
        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(
            retry_policy=retry_policy, fail_max=3, reset_timeout=0.1
        )

        failing_func = Mock(side_effect=Exception("Failed"))
        success_func = Mock(return_value="success")

        # State 1: CLOSED
        assert cb_retry.get_state() == "closed"

        # Trigger failures to open circuit
        for _ in range(3):
            with pytest.raises(Exception):
                cb_retry.execute(failing_func)

        # State 2: OPEN
        assert cb_retry.get_state() == "open"

        # Verify fast-fail
        with pytest.raises(CircuitBreakerError):
            cb_retry.execute(success_func)

        # Wait for timeout
        time.sleep(0.15)

        # State 3: HALF_OPEN (transitions during successful call)
        result = cb_retry.execute(success_func)
        assert result == "success"

        # State 4: CLOSED
        assert cb_retry.get_state() == "closed"

    def test_manual_reset_works_in_integration(self):
        """Test manual reset functionality."""
        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(
            retry_policy=retry_policy,
            fail_max=2,
            reset_timeout=60,  # Long timeout
        )

        failing_func = Mock(side_effect=Exception("Failed"))
        success_func = Mock(return_value="success")

        # Open the circuit
        for _ in range(2):
            with pytest.raises(Exception):
                cb_retry.execute(failing_func)

        assert cb_retry.get_state() == "open"

        # Manual reset
        cb_retry.reset()
        assert cb_retry.get_state() == "closed"

        # Should work normally now
        result = cb_retry.execute(success_func)
        assert result == "success"


class TestCircuitBreakerPerformanceIntegration:
    """Test circuit breaker performance in realistic scenarios."""

    def test_fast_fail_prevents_retry_attempts(self):
        """Test circuit breaker prevents expensive retry attempts when open."""
        retry_policy = RetryPolicy(max_attempts=5, base_delay=0.1)  # Would take ~1.5s total
        cb_retry = CircuitBreakerRetryPolicy(retry_policy=retry_policy, fail_max=1, reset_timeout=1)

        slow_failing_func = Mock(side_effect=Exception("Network timeout"))

        # First failure opens circuit (will do all retry attempts)
        with pytest.raises(Exception):
            cb_retry.execute(slow_failing_func)

        assert cb_retry.get_state() == "open"
        initial_call_count = slow_failing_func.call_count  # Should be 5 after retries

        # Next call should fast-fail without retrying
        start = time.time()
        with pytest.raises(CircuitBreakerError):
            cb_retry.execute(slow_failing_func)
        fast_fail_time = time.time() - start

        # Should be much faster than retry attempts would be
        assert fast_fail_time < 0.01  # 10ms threshold (much less than 1.5s)

        # No additional calls should have been made (circuit breaker prevented retry)
        assert slow_failing_func.call_count == initial_call_count


class TestCircuitBreakerMonitoring:
    """Test circuit breaker monitoring and statistics."""

    def test_statistics_track_state_transitions(self):
        """Test stats accurately track state transitions."""
        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(
            retry_policy=retry_policy, fail_max=2, reset_timeout=0.1
        )

        failing_func = Mock(side_effect=Exception("Failed"))
        success_func = Mock(return_value="success")

        # Initial stats
        stats = cb_retry.get_stats()
        assert stats["state"] == "closed"
        assert stats["failure_count"] == 0

        # Open circuit
        for _ in range(2):
            with pytest.raises(Exception):
                cb_retry.execute(failing_func)

        stats = cb_retry.get_stats()
        assert stats["state"] == "open"
        assert stats["failure_count"] == 2

        # Wait and recover
        time.sleep(0.15)
        cb_retry.execute(success_func)

        stats = cb_retry.get_stats()
        assert stats["state"] == "closed"
        # Note: pybreaker resets failure_count when circuit closes


class TestCircuitBreakerOTELMetrics:
    """Test circuit breaker OTEL metrics integration (Code Review Issue #4)."""

    def test_circuit_breaker_metrics_exported_via_otel(self):
        """Test that circuit breaker metrics appear in OTEL metric exports."""
        from mca_sdk.buffering.exporters import BufferedOTLPMetricExporter
        from opentelemetry.sdk.metrics import MeterProvider
        from opentelemetry.sdk.metrics.export import InMemoryMetricReader
        from opentelemetry.sdk.resources import Resource

        # Create in-memory metric reader to capture exports
        metric_reader = InMemoryMetricReader()
        resource = Resource.create({"service.name": "test-circuit-metrics"})
        meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
        meter = meter_provider.get_meter("test-meter")

        # Create circuit breaker and set meter
        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(
            retry_policy=retry_policy,
            fail_max=2,
            reset_timeout=1,
            name="test_otel_metrics",
        )
        cb_retry.set_meter(meter)

        # Trigger state transition (closed → open)
        failing_func = Mock(side_effect=Exception("Failed"))
        for _ in range(2):
            with pytest.raises(Exception):
                cb_retry.execute(failing_func)

        assert cb_retry.get_state() == "open"

        # Force metric collection
        metrics_data = metric_reader.get_metrics_data()

        # Verify circuit breaker metrics exist in export
        metric_names = set()
        for resource_metrics in metrics_data.resource_metrics:
            for scope_metrics in resource_metrics.scope_metrics:
                for metric in scope_metrics.metrics:
                    metric_names.add(metric.name)

        # Verify all three circuit breaker metrics are present
        assert (
            "circuit_breaker.state" in metric_names
        ), "circuit_breaker.state metric not found in OTEL export"
        assert (
            "circuit_breaker.transitions" in metric_names
        ), "circuit_breaker.transitions metric not found in OTEL export"
        assert (
            "circuit_breaker.failures" in metric_names
        ), "circuit_breaker.failures metric not found in OTEL export"

    def test_circuit_breaker_state_metric_values(self):
        """Test that circuit_breaker.state metric reports correct state values."""
        from opentelemetry.sdk.metrics import MeterProvider
        from opentelemetry.sdk.metrics.export import InMemoryMetricReader
        from opentelemetry.sdk.resources import Resource

        metric_reader = InMemoryMetricReader()
        resource = Resource.create({"service.name": "test-state-values"})
        meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
        meter = meter_provider.get_meter("test-meter")

        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(
            retry_policy=retry_policy,
            fail_max=2,
            reset_timeout=0.1,
            name="test_state_values",
        )
        cb_retry.set_meter(meter)

        # Initial state: CLOSED (0)
        metrics_data = metric_reader.get_metrics_data()
        state_value = _extract_metric_value(metrics_data, "circuit_breaker.state")
        assert state_value == 0, f"Expected state=0 (closed), got {state_value}"

        # Open circuit
        failing_func = Mock(side_effect=Exception("Failed"))
        for _ in range(2):
            with pytest.raises(Exception):
                cb_retry.execute(failing_func)

        # State: OPEN (2)
        metrics_data = metric_reader.get_metrics_data()
        state_value = _extract_metric_value(metrics_data, "circuit_breaker.state")
        assert state_value == 2, f"Expected state=2 (open), got {state_value}"

    def test_circuit_breaker_transition_counter_increments(self):
        """Test that circuit_breaker.transitions counter increments on state changes."""
        from opentelemetry.sdk.metrics import MeterProvider
        from opentelemetry.sdk.metrics.export import InMemoryMetricReader
        from opentelemetry.sdk.resources import Resource

        metric_reader = InMemoryMetricReader()
        resource = Resource.create({"service.name": "test-transitions"})
        meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
        meter = meter_provider.get_meter("test-meter")

        retry_policy = RetryPolicy(max_attempts=1, base_delay=0.001)
        cb_retry = CircuitBreakerRetryPolicy(
            retry_policy=retry_policy,
            fail_max=2,
            reset_timeout=0.1,
            name="test_transitions",
        )
        cb_retry.set_meter(meter)

        # Initial: No transitions yet
        metrics_data = metric_reader.get_metrics_data()
        initial_transitions = (
            _extract_metric_value(metrics_data, "circuit_breaker.transitions") or 0
        )

        # Trigger state transition: closed → open
        failing_func = Mock(side_effect=Exception("Failed"))
        for _ in range(2):
            with pytest.raises(Exception):
                cb_retry.execute(failing_func)

        # Verify transition counter incremented
        metrics_data = metric_reader.get_metrics_data()
        after_open_transitions = _extract_metric_value(metrics_data, "circuit_breaker.transitions")
        assert (
            after_open_transitions > initial_transitions
        ), f"Transitions should have incremented from {initial_transitions} to {after_open_transitions}"

        # Wait and recover (open → half_open → closed)
        time.sleep(0.15)
        success_func = Mock(return_value="success")
        cb_retry.execute(success_func)

        # Verify transitions metric exists and has recorded at least one transition
        # Note: pybreaker may batch or aggregate some transitions, so we verify
        # that the metric is being recorded rather than asserting exact counts
        metrics_data = metric_reader.get_metrics_data()
        final_transitions = _extract_metric_value(metrics_data, "circuit_breaker.transitions")
        assert (
            final_transitions >= after_open_transitions
        ), "Transitions metric should continue to be recorded"
        assert final_transitions >= 1, "At least one transition should be recorded"


def _extract_metric_value(metrics_data, metric_name: str):
    """Helper to extract a metric value from InMemoryMetricReader data."""
    for resource_metrics in metrics_data.resource_metrics:
        for scope_metrics in resource_metrics.scope_metrics:
            for metric in scope_metrics.metrics:
                if metric.name == metric_name:
                    # Handle different metric types
                    for data_point in metric.data.data_points:
                        return data_point.value
    return None
