"""Unit tests for recovery engine."""

import asyncio
import pytest
from aperion_flow.engine.recovery import (
    CircuitBreaker,
    CircuitState,
    ErrorClassifier,
    FailureType,
    RecoveryConfig,
    RecoveryEngine,
    RecoveryResult,
    RecoveryStrategy,
)


class TestErrorClassifier:
    """Tests for ErrorClassifier."""

    def test_classify_transient_errors(self) -> None:
        """Test classification of transient errors."""
        assert ErrorClassifier.classify("Connection timeout") == FailureType.TRANSIENT
        assert ErrorClassifier.classify("Connection refused") == FailureType.TRANSIENT
        assert ErrorClassifier.classify("503 Service Unavailable") == FailureType.TRANSIENT
        assert ErrorClassifier.classify("Rate limit exceeded") == FailureType.TRANSIENT

    def test_classify_permanent_errors(self) -> None:
        """Test classification of permanent errors."""
        assert ErrorClassifier.classify("Invalid input") == FailureType.PERMANENT
        assert ErrorClassifier.classify("401 Unauthorized") == FailureType.PERMANENT
        assert ErrorClassifier.classify("403 Forbidden") == FailureType.PERMANENT
        assert ErrorClassifier.classify("Validation error") == FailureType.PERMANENT

    def test_classify_resource_errors(self) -> None:
        """Test classification of resource errors."""
        assert ErrorClassifier.classify("Out of memory") == FailureType.RESOURCE
        assert ErrorClassifier.classify("No space left on device") == FailureType.RESOURCE
        assert ErrorClassifier.classify("Disk full") == FailureType.RESOURCE

    def test_classify_unknown_errors(self) -> None:
        """Test classification of unknown errors."""
        assert ErrorClassifier.classify("Some random error") == FailureType.UNKNOWN
        assert ErrorClassifier.classify(Exception("generic")) == FailureType.UNKNOWN


class TestCircuitBreaker:
    """Tests for CircuitBreaker."""

    def test_initial_state(self) -> None:
        """Test circuit breaker starts closed."""
        cb = CircuitBreaker()
        assert cb.state == CircuitState.CLOSED
        assert cb.can_execute()

    def test_opens_after_threshold(self) -> None:
        """Test circuit opens after failure threshold."""
        cb = CircuitBreaker(failure_threshold=3)

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

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

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

    def test_resets_on_success(self) -> None:
        """Test circuit resets on success."""
        cb = CircuitBreaker(failure_threshold=3)

        cb.record_failure()
        cb.record_failure()
        cb.record_success()

        assert cb.state == CircuitState.CLOSED
        assert cb._failure_count == 0

    def test_half_open_after_timeout(self) -> None:
        """Test circuit becomes half-open after timeout."""
        cb = CircuitBreaker(failure_threshold=1, reset_timeout=0.1)

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

        # Wait for timeout
        import time
        time.sleep(0.15)

        assert cb.state == CircuitState.HALF_OPEN
        assert cb.can_execute()

    def test_reset_method(self) -> None:
        """Test manual reset."""
        cb = CircuitBreaker(failure_threshold=1)
        cb.record_failure()
        assert cb.state == CircuitState.OPEN

        cb.reset()
        assert cb.state == CircuitState.CLOSED
        assert cb._failure_count == 0


class TestRecoveryConfig:
    """Tests for RecoveryConfig."""

    def test_default_config(self) -> None:
        """Test default configuration values."""
        config = RecoveryConfig()
        assert config.strategy == RecoveryStrategy.EXPONENTIAL_BACKOFF
        assert config.max_retries == 3
        assert config.base_delay == 1.0
        assert config.exponential_base == 2.0
        assert config.jitter is True

    def test_custom_config(self) -> None:
        """Test custom configuration."""
        config = RecoveryConfig(
            strategy=RecoveryStrategy.FIXED_BACKOFF,
            max_retries=5,
            base_delay=2.0,
            jitter=False,
        )
        assert config.strategy == RecoveryStrategy.FIXED_BACKOFF
        assert config.max_retries == 5
        assert config.base_delay == 2.0
        assert config.jitter is False


class TestRecoveryEngine:
    """Tests for RecoveryEngine."""

    @pytest.fixture
    def engine(self) -> RecoveryEngine:
        return RecoveryEngine(RecoveryConfig(
            max_retries=3,
            base_delay=0.01,  # Fast for tests
            jitter=False,
        ))

    @pytest.mark.asyncio
    async def test_success_on_first_try(self, engine: RecoveryEngine) -> None:
        """Test successful execution on first try."""
        call_count = 0

        async def handler() -> dict:
            nonlocal call_count
            call_count += 1
            return {"result": "success"}

        result = await engine.execute_with_recovery(handler)

        assert result.success
        assert result.data == {"result": "success"}
        assert len(result.attempts) == 1
        assert call_count == 1

    @pytest.mark.asyncio
    async def test_success_after_retries(self, engine: RecoveryEngine) -> None:
        """Test success after some failures."""
        call_count = 0

        async def handler() -> dict:
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise ConnectionError("Timeout")
            return {"result": "success"}

        result = await engine.execute_with_recovery(handler)

        assert result.success
        assert call_count == 3
        assert len(result.attempts) == 3

    @pytest.mark.asyncio
    async def test_failure_after_max_retries(self, engine: RecoveryEngine) -> None:
        """Test failure after exhausting retries."""
        async def handler() -> dict:
            raise ConnectionError("Always fails")

        result = await engine.execute_with_recovery(handler)

        assert not result.success
        assert result.final_error == "Always fails"
        assert len(result.attempts) == 4  # Initial + 3 retries

    @pytest.mark.asyncio
    async def test_no_retry_on_permanent_error(self, engine: RecoveryEngine) -> None:
        """Test that permanent errors are not retried."""
        call_count = 0

        async def handler() -> dict:
            nonlocal call_count
            call_count += 1
            raise ValueError("Invalid input - permanent error")

        result = await engine.execute_with_recovery(handler)

        assert not result.success
        assert call_count == 1  # No retries for permanent errors
        assert len(result.attempts) == 1

    @pytest.mark.asyncio
    async def test_fallback_handler(self) -> None:
        """Test fallback handler is used on failure."""
        engine = RecoveryEngine(RecoveryConfig(
            max_retries=1,
            base_delay=0.01,
        ))

        async def primary() -> dict:
            raise ValueError("Primary fails")

        async def fallback() -> dict:
            return {"source": "fallback"}

        result = await engine.execute_with_recovery(primary, fallback=fallback)

        assert result.success
        assert result.used_fallback
        assert result.data == {"source": "fallback"}

    @pytest.mark.asyncio
    async def test_circuit_breaker_integration(self) -> None:
        """Test circuit breaker blocks execution."""
        engine = RecoveryEngine(RecoveryConfig(
            failure_threshold=2,
            max_retries=0,
        ))

        async def handler() -> dict:
            raise ConnectionError("Fails")

        # First two failures open the circuit
        await engine.execute_with_recovery(handler, circuit_key="test")
        await engine.execute_with_recovery(handler, circuit_key="test")

        # Third call should be blocked by circuit
        result = await engine.execute_with_recovery(handler, circuit_key="test")

        assert not result.success
        assert result.circuit_opened
        assert "Circuit breaker is open" in (result.final_error or "")

    def test_calculate_delay_immediate(self) -> None:
        """Test delay calculation for immediate retry."""
        engine = RecoveryEngine(RecoveryConfig(
            strategy=RecoveryStrategy.IMMEDIATE_RETRY,
        ))
        assert engine.calculate_delay(0) == 0.0
        assert engine.calculate_delay(5) == 0.0

    def test_calculate_delay_fixed(self) -> None:
        """Test delay calculation for fixed backoff."""
        engine = RecoveryEngine(RecoveryConfig(
            strategy=RecoveryStrategy.FIXED_BACKOFF,
            base_delay=2.0,
            jitter=False,
        ))
        assert engine.calculate_delay(0) == 2.0
        assert engine.calculate_delay(5) == 2.0

    def test_calculate_delay_exponential(self) -> None:
        """Test delay calculation for exponential backoff."""
        engine = RecoveryEngine(RecoveryConfig(
            strategy=RecoveryStrategy.EXPONENTIAL_BACKOFF,
            base_delay=1.0,
            exponential_base=2.0,
            max_delay=60.0,
            jitter=False,
        ))
        assert engine.calculate_delay(0) == 1.0  # 1 * 2^0
        assert engine.calculate_delay(1) == 2.0  # 1 * 2^1
        assert engine.calculate_delay(2) == 4.0  # 1 * 2^2
        assert engine.calculate_delay(3) == 8.0  # 1 * 2^3

    def test_calculate_delay_capped(self) -> None:
        """Test delay is capped at max_delay."""
        engine = RecoveryEngine(RecoveryConfig(
            strategy=RecoveryStrategy.EXPONENTIAL_BACKOFF,
            base_delay=1.0,
            exponential_base=2.0,
            max_delay=10.0,
            jitter=False,
        ))
        # 1 * 2^10 = 1024, but capped at 10
        assert engine.calculate_delay(10) == 10.0

    def test_should_retry(self) -> None:
        """Test retry decision logic."""
        engine = RecoveryEngine(RecoveryConfig(max_retries=3))

        assert engine.should_retry(FailureType.TRANSIENT, 0)
        assert engine.should_retry(FailureType.TRANSIENT, 2)
        assert not engine.should_retry(FailureType.TRANSIENT, 3)  # Max reached
        assert not engine.should_retry(FailureType.PERMANENT, 0)  # Never retry permanent

    def test_get_stats(self) -> None:
        """Test statistics retrieval."""
        engine = RecoveryEngine(RecoveryConfig(
            strategy=RecoveryStrategy.EXPONENTIAL_BACKOFF,
            max_retries=5,
        ))

        stats = engine.get_stats()
        assert stats["strategy"] == "exponential_backoff"
        assert stats["max_retries"] == 5
        assert "circuit_breakers" in stats


class TestRecoveryResult:
    """Tests for RecoveryResult."""

    def test_success_result(self) -> None:
        """Test successful result structure."""
        result = RecoveryResult(
            success=True,
            data={"key": "value"},
        )
        assert result.success
        assert result.data == {"key": "value"}
        assert result.final_error is None

    def test_failure_result(self) -> None:
        """Test failure result structure."""
        result = RecoveryResult(
            success=False,
            final_error="All retries exhausted",
        )
        assert not result.success
        assert result.final_error == "All retries exhausted"

    def test_result_with_fallback(self) -> None:
        """Test result with fallback used."""
        result = RecoveryResult(
            success=True,
            data={"source": "fallback"},
            used_fallback=True,
        )
        assert result.success
        assert result.used_fallback
