"""E2E tests for resilience — retry, circuit breaker, and fallback routing."""

from __future__ import annotations

import time
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from claim_validator.clearinghouse.base import BaseClearinghouseClient
from claim_validator.clearinghouse.exceptions import ClearinghouseServerError
from claim_validator.clearinghouse.models import SubmissionResult
from claim_validator.clearinghouse.pool import ClearinghouseClientPool
from claim_validator.exceptions import ConfigurationError
from claim_validator.resilience.circuit_breaker import (
    CircuitBreaker,
    CircuitOpenError,
    CircuitState,
)
from claim_validator.resilience.retry import _is_retryable, retry_clearinghouse
from claim_validator.routing.models import PayerRoute
from claim_validator.routing.router import PayerRouter


def _mock_client(name: str) -> MagicMock:
    c = MagicMock(spec=BaseClearinghouseClient)
    c.provider_name = name
    return c


class TestCircuitBreakerEndToEnd:
    """E2E: circuit breaker state transitions through full lifecycle."""

    def test_full_lifecycle_closed_open_halfopen_closed(self) -> None:
        """CLOSED -> OPEN (5 failures) -> HALF_OPEN (timeout) -> CLOSED (success)."""
        cb = CircuitBreaker(failure_threshold=3, recovery_timeout=0.02, failure_window=60.0)

        # Start closed
        assert cb.state == CircuitState.CLOSED

        # 3 failures -> OPEN
        for _ in range(3):
            cb.record_failure()
        assert cb.state == CircuitState.OPEN

        # check() should raise
        with pytest.raises(CircuitOpenError):
            cb.check()

        # Wait for recovery -> HALF_OPEN
        time.sleep(0.03)
        assert cb.state == CircuitState.HALF_OPEN
        cb.check()  # Should not raise in HALF_OPEN

        # Success -> CLOSED
        cb.record_success()
        assert cb.state == CircuitState.CLOSED

    def test_halfopen_failure_reopens(self) -> None:
        """HALF_OPEN -> failure -> immediately back to OPEN."""
        cb = CircuitBreaker(failure_threshold=2, recovery_timeout=0.01)
        cb.record_failure()
        cb.record_failure()
        time.sleep(0.02)
        assert cb.state == CircuitState.HALF_OPEN

        cb.record_failure()
        assert cb.state == CircuitState.OPEN


class TestRetryEndToEnd:
    """E2E: retry decorator behavior."""

    def test_retryable_exceptions_identified(self) -> None:
        assert _is_retryable(ClearinghouseServerError("500")) is True
        assert _is_retryable(ValueError("bad")) is False

    def test_decorated_function_works(self) -> None:
        @retry_clearinghouse
        def ok() -> str:
            return "ok"

        assert ok() == "ok"

    def test_non_retryable_exception_propagates(self) -> None:
        @retry_clearinghouse
        def bad() -> None:
            raise ValueError("not retryable")

        with pytest.raises(ValueError, match="not retryable"):
            bad()


class TestFallbackRoutingEndToEnd:
    """E2E: pool fallback routing when circuits open."""

    @patch("claim_validator.clearinghouse.pool.get_clearinghouse_client")
    def test_fallback_on_circuit_open(self, mock_factory: MagicMock) -> None:
        """Primary circuit open -> fallback to secondary clearinghouse."""
        primary = _mock_client("stedi")
        primary.submit_claim.side_effect = CircuitOpenError("stedi down")
        fallback = _mock_client("claimmd")
        fallback.submit_claim.return_value = SubmissionResult(
            status="accepted", accepted=True, reference_id="FALLBACK-001",
        )
        mock_factory.side_effect = [primary, fallback]

        custom = {
            "FB_PAYER": [
                PayerRoute("stedi", "FB1", 1, frozenset({"837P"})),
                PayerRoute("claimmd", "FB2", 2, frozenset({"837P"})),
            ],
        }
        pool = ClearinghouseClientPool(
            configs={"stedi": {"api_key": "k1"}, "claimmd": {"account_key": "k2"}},
            router=PayerRouter(custom_mappings=custom),
        )

        result = pool.submit_claim({"npi": "1234567893"}, "FB_PAYER")
        assert result.reference_id == "FALLBACK-001"
        assert result.accepted is True

    @patch("claim_validator.clearinghouse.pool.get_clearinghouse_client")
    def test_all_circuits_open_raises(self, mock_factory: MagicMock) -> None:
        """All clearinghouses down -> ConfigurationError."""
        c1 = _mock_client("stedi")
        c1.submit_claim.side_effect = CircuitOpenError("down")
        c2 = _mock_client("claimmd")
        c2.submit_claim.side_effect = CircuitOpenError("down")
        mock_factory.side_effect = [c1, c2]

        custom = {
            "ALL_DOWN": [
                PayerRoute("stedi", "A1", 1, frozenset({"837P"})),
                PayerRoute("claimmd", "A2", 2, frozenset({"837P"})),
            ],
        }
        pool = ClearinghouseClientPool(
            configs={"stedi": {"api_key": "k"}, "claimmd": {"account_key": "k"}},
            router=PayerRouter(custom_mappings=custom),
        )

        with pytest.raises(ConfigurationError, match="unavailable"):
            pool.submit_claim({"npi": "1234567893"}, "ALL_DOWN")

    @patch("claim_validator.clearinghouse.pool.get_clearinghouse_client")
    def test_status_queries_no_fallback(self, mock_factory: MagicMock) -> None:
        """Claim status always goes to primary — no fallback routing."""
        primary = _mock_client("stedi")
        primary.check_claim_status.return_value = MagicMock()
        mock_factory.return_value = primary

        custom = {"STATUS_P": [
            PayerRoute("stedi", "S1", 1, frozenset({"837P"})),
            PayerRoute("claimmd", "S2", 2, frozenset({"837P"})),
        ]}
        pool = ClearinghouseClientPool(
            configs={"stedi": {"api_key": "k"}, "claimmd": {"account_key": "k"}},
            router=PayerRouter(custom_mappings=custom),
        )

        pool.check_claim_status("REF-1", "STATUS_P")
        primary.check_claim_status.assert_called_once_with("REF-1")
