"""
Tests for circuit breaker thread safety (P2-4).

Covers:
    - Concurrent exports don't corrupt circuit breaker state
    - Circuit opens after threshold failures
    - Circuit resets on success
    - Thread-safe state transitions
"""

from __future__ import annotations

import threading
import time
from typing import Any, Dict, List, Optional
from unittest.mock import patch, MagicMock

import pytest

from risicare.exporters.base import ExportResult
from risicare.exporters.http import HttpExporter
from risicare.exporters.otlp import OTLPExporter


# =============================================================================
# Helpers
# =============================================================================


class _FakeSpan:
    """Minimal span-like object for testing exporters."""

    def __init__(self, trace_id: str = "a" * 32, span_id: str = "b" * 16):
        from datetime import datetime, timezone
        self.trace_id = trace_id
        self.span_id = span_id
        self.name = "test"
        self.parent_span_id = None
        self.start_time = datetime.now(timezone.utc)
        self.end_time = datetime.now(timezone.utc)
        self.status = MagicMock(value="ok")
        self.status_message = None
        self.kind = MagicMock(value="internal")
        self.attributes = {}
        self.events = []
        self.links = []
        self.exceptions = []
        self.llm = None
        self.agent_id = None
        self.session_id = None
        self.semantic_phase = None

    def to_dict(self) -> Dict[str, Any]:
        return {"trace_id": self.trace_id, "span_id": self.span_id}


# =============================================================================
# HttpExporter Circuit Breaker Tests
# =============================================================================


class TestHttpCircuitBreakerThreadSafety:
    """HttpExporter circuit breaker must be thread-safe (P2-4)."""

    def test_circuit_opens_after_threshold(self):
        """Circuit should open after 3 consecutive failures."""
        exporter = HttpExporter(endpoint="http://unreachable.invalid:1")
        spans = [_FakeSpan()]

        # Mock _send_request to always fail
        with patch.object(exporter, "_send_request", return_value=ExportResult.FAILURE):
            # First 3 calls should attempt and fail (incrementing counter)
            for _ in range(3):
                result = exporter.export(spans)
                assert result == ExportResult.FAILURE

            # After 3 failures, circuit is open — should return FAILURE immediately
            result = exporter.export(spans)
            assert result == ExportResult.FAILURE

    def test_circuit_resets_on_success(self):
        """Success should reset the failure counter."""
        exporter = HttpExporter(endpoint="http://unreachable.invalid:1")
        spans = [_FakeSpan()]

        # Accumulate 2 failures (below threshold of 3)
        with patch.object(exporter, "_send_request", return_value=ExportResult.FAILURE):
            exporter.export(spans)
            exporter.export(spans)

        assert exporter._consecutive_failures == 2

        # Now succeed — should reset counter
        with patch.object(exporter, "_send_request", return_value=ExportResult.SUCCESS):
            result = exporter.export(spans)
            assert result == ExportResult.SUCCESS

        with exporter._cb_lock:
            assert exporter._consecutive_failures == 0

    def test_concurrent_exports_dont_corrupt_state(self):
        """10 threads x 20 exports each — state should remain consistent."""
        exporter = HttpExporter(
            endpoint="http://unreachable.invalid:1",
            max_retries=1,  # Fast failure
        )
        spans = [_FakeSpan()]

        # Always fail to exercise the failure path
        with patch.object(exporter, "_send_request", return_value=ExportResult.FAILURE):
            errors: List[Exception] = []

            def worker():
                try:
                    for _ in range(20):
                        exporter.export(spans)
                except Exception as e:
                    errors.append(e)

            threads = [threading.Thread(target=worker) for _ in range(10)]
            for t in threads:
                t.start()
            for t in threads:
                t.join(timeout=30)

        # No exceptions should have occurred
        assert not errors, f"Thread errors: {errors}"

        # State should be consistent (no negative counters)
        with exporter._cb_lock:
            assert exporter._consecutive_failures >= 0
            assert isinstance(exporter._circuit_open_until, float)

    def test_half_open_after_cooldown(self):
        """After cooldown, circuit should allow one attempt (half-open)."""
        exporter = HttpExporter(endpoint="http://unreachable.invalid:1")
        exporter._circuit_breaker_cooldown = 0.01  # 10ms for fast test
        spans = [_FakeSpan()]

        # Open the circuit
        with exporter._cb_lock:
            exporter._consecutive_failures = 3
            exporter._circuit_open_until = time.monotonic() + 0.01

        # Should be blocked right now
        with patch.object(exporter, "_send_request", return_value=ExportResult.SUCCESS) as mock:
            result = exporter.export(spans)
            assert result == ExportResult.FAILURE
            mock.assert_not_called()

        # Wait for cooldown
        time.sleep(0.02)

        # Should be allowed (half-open)
        with patch.object(exporter, "_send_request", return_value=ExportResult.SUCCESS):
            result = exporter.export(spans)
            assert result == ExportResult.SUCCESS

        with exporter._cb_lock:
            assert exporter._consecutive_failures == 0


# =============================================================================
# OTLPExporter Circuit Breaker Tests
# =============================================================================


class TestOTLPCircuitBreakerThreadSafety:
    """OTLPExporter circuit breaker must be thread-safe (P2-4)."""

    def test_circuit_opens_after_threshold(self):
        """Circuit should open after 3 consecutive failures."""
        exporter = OTLPExporter(endpoint="http://unreachable.invalid:1/v1/traces")
        spans = [_FakeSpan()]

        # Mock _send to always fail
        with patch.object(exporter, "_send", return_value=False):
            for _ in range(3):
                result = exporter.export(spans)
                assert result == ExportResult.FAILURE

            # Circuit now open
            result = exporter.export(spans)
            assert result == ExportResult.FAILURE

    def test_circuit_resets_on_success(self):
        """Success should reset the failure counter."""
        exporter = OTLPExporter(endpoint="http://unreachable.invalid:1/v1/traces")
        spans = [_FakeSpan()]

        # Accumulate 2 failures
        with patch.object(exporter, "_send", return_value=False):
            exporter.export(spans)
            exporter.export(spans)

        assert exporter._consecutive_failures == 2

        # Succeed
        with patch.object(exporter, "_send", return_value=True):
            result = exporter.export(spans)
            assert result == ExportResult.SUCCESS

        with exporter._cb_lock:
            assert exporter._consecutive_failures == 0

    def test_concurrent_exports_dont_corrupt_state(self):
        """10 threads x 20 exports each — state should remain consistent."""
        exporter = OTLPExporter(
            endpoint="http://unreachable.invalid:1/v1/traces",
            max_retries=1,
        )
        spans = [_FakeSpan()]

        with patch.object(exporter, "_send", return_value=False):
            errors: List[Exception] = []

            def worker():
                try:
                    for _ in range(20):
                        exporter.export(spans)
                except Exception as e:
                    errors.append(e)

            threads = [threading.Thread(target=worker) for _ in range(10)]
            for t in threads:
                t.start()
            for t in threads:
                t.join(timeout=30)

        assert not errors, f"Thread errors: {errors}"

        with exporter._cb_lock:
            assert exporter._consecutive_failures >= 0
            assert isinstance(exporter._circuit_open_until, float)

    def test_half_open_after_cooldown(self):
        """After cooldown, circuit should allow retry."""
        exporter = OTLPExporter(endpoint="http://unreachable.invalid:1/v1/traces")
        exporter._cooldown_seconds = 0.01
        spans = [_FakeSpan()]

        # Open circuit
        with exporter._cb_lock:
            exporter._consecutive_failures = 3
            exporter._circuit_open_until = time.monotonic() + 0.01

        # Blocked
        with patch.object(exporter, "_send", return_value=True) as mock:
            result = exporter.export(spans)
            assert result == ExportResult.FAILURE

        # Wait for cooldown
        time.sleep(0.02)

        # Half-open: allowed
        with patch.object(exporter, "_send", return_value=True):
            result = exporter.export(spans)
            assert result == ExportResult.SUCCESS

        with exporter._cb_lock:
            assert exporter._consecutive_failures == 0
