"""Tests for connection manager with retry and recovery."""

import asyncio
from unittest.mock import AsyncMock, MagicMock

import pytest

from rpipe.connection_manager import (
    CircuitBreaker,
    CircuitBreakerConfig,
    CircuitState,
    ConnectionManager,
    RetryConfig,
)


class TestCircuitBreaker:
    """Test circuit breaker state machine."""

    def test_create_circuit_breaker(self):
        """Create a circuit breaker."""
        breaker = CircuitBreaker("example.com")
        assert breaker.host == "example.com"
        assert breaker.state == CircuitState.CLOSED
        assert breaker.failure_count == 0

    def test_should_attempt_closed(self):
        """In CLOSED state, should always attempt."""
        breaker = CircuitBreaker("example.com")
        assert breaker.should_attempt() is True

    def test_should_attempt_open_before_timeout(self):
        """In OPEN state before timeout, should not attempt."""
        breaker = CircuitBreaker("example.com", CircuitBreakerConfig(recovery_timeout_s=10))
        breaker._open_circuit()
        assert breaker.should_attempt() is False

    def test_should_attempt_half_open(self):
        """In HALF_OPEN state, should attempt."""
        breaker = CircuitBreaker("example.com")
        breaker._try_recover()
        assert breaker.should_attempt() is True

    def test_record_success_in_closed(self):
        """Recording success in CLOSED keeps it closed."""
        breaker = CircuitBreaker("example.com")
        breaker.record_success()
        assert breaker.state == CircuitState.CLOSED
        assert breaker.failure_count == 0

    def test_record_success_in_half_open(self):
        """Successes in HALF_OPEN transition toward CLOSED."""
        config = CircuitBreakerConfig(success_threshold=2)
        breaker = CircuitBreaker("example.com", config)
        breaker._try_recover()

        breaker.record_success()
        assert breaker.state == CircuitState.HALF_OPEN
        assert breaker.success_count == 1

        breaker.record_success()
        assert breaker.state == CircuitState.CLOSED

    def test_record_failure_in_closed(self):
        """Failures in CLOSED increment count."""
        config = CircuitBreakerConfig(failure_threshold=3)
        breaker = CircuitBreaker("example.com", config)

        breaker.record_failure()
        assert breaker.state == CircuitState.CLOSED
        assert breaker.failure_count == 1

    def test_record_failure_opens_circuit(self):
        """Reaching failure threshold opens circuit."""
        config = CircuitBreakerConfig(failure_threshold=3)
        breaker = CircuitBreaker("example.com", config)

        breaker.record_failure()
        breaker.record_failure()
        breaker.record_failure()

        assert breaker.state == CircuitState.OPEN

    def test_record_failure_in_half_open(self):
        """Failure in HALF_OPEN reopens circuit."""
        breaker = CircuitBreaker("example.com")
        breaker._try_recover()
        assert breaker.state == CircuitState.HALF_OPEN

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

    def test_get_state(self):
        """Get state as string."""
        breaker = CircuitBreaker("example.com")
        assert breaker.get_state() == "closed"

        breaker._open_circuit()
        assert breaker.get_state() == "open"

        breaker._try_recover()
        assert breaker.get_state() == "half_open"


class TestRetryConfig:
    """Test retry configuration."""

    def test_default_retry_config(self):
        """Default retry config has reasonable values."""
        config = RetryConfig()
        assert config.max_attempts == 3
        assert config.initial_delay_s == 0.1
        assert config.max_delay_s == 30.0
        assert config.exponential_base == 2.0

    def test_custom_retry_config(self):
        """Create custom retry config."""
        config = RetryConfig(max_attempts=5, initial_delay_s=0.5, max_delay_s=60.0)
        assert config.max_attempts == 5
        assert config.initial_delay_s == 0.5
        assert config.max_delay_s == 60.0


@pytest.mark.asyncio
class TestConnectionManagerRetry:
    """Test connection manager retry logic."""

    async def test_execute_success_on_first_attempt(self):
        """Successful operation on first attempt."""
        manager = ConnectionManager()
        operation = AsyncMock(return_value="success")

        result = await manager.execute_with_retry("example.com", operation, arg1="value")

        assert result == "success"
        assert operation.call_count == 1

    async def test_execute_retry_on_failure(self):
        """Retry after transient failure."""
        manager = ConnectionManager(RetryConfig(max_attempts=3, initial_delay_s=0.01))
        operation = AsyncMock(side_effect=[Exception("fail1"), Exception("fail2"), "success"])

        result = await manager.execute_with_retry("example.com", operation)

        assert result == "success"
        assert operation.call_count == 3

    async def test_execute_fails_after_max_attempts(self):
        """Raise error after exhausting retries."""
        manager = ConnectionManager(RetryConfig(max_attempts=3, initial_delay_s=0.01))
        operation = AsyncMock(side_effect=Exception("always fails"))

        with pytest.raises(ConnectionError):
            await manager.execute_with_retry("example.com", operation)

        assert operation.call_count == 3

    async def test_execute_records_success(self):
        """Recording success in circuit breaker."""
        manager = ConnectionManager()
        operation = AsyncMock(return_value="ok")

        await manager.execute_with_retry("example.com", operation)

        breaker = manager.get_circuit_breaker("example.com")
        assert breaker.failure_count == 0
        assert breaker.state == CircuitState.CLOSED

    async def test_execute_records_failure(self):
        """Recording failures in circuit breaker."""
        config = CircuitBreakerConfig(failure_threshold=3)
        manager = ConnectionManager(
            retry_config=RetryConfig(max_attempts=1),  # Single attempt per call
            circuit_breaker_config=config,
        )
        operation = AsyncMock(side_effect=Exception("fail"))

        # First two failures don't open circuit
        for _ in range(2):
            with pytest.raises(ConnectionError):
                await manager.execute_with_retry("example.com", operation)

        breaker = manager.get_circuit_breaker("example.com")
        assert breaker.failure_count == 2
        assert breaker.state == CircuitState.CLOSED

        # Third failure opens circuit
        with pytest.raises(ConnectionError):
            await manager.execute_with_retry("example.com", operation)

        assert breaker.failure_count == 3
        assert breaker.state == CircuitState.OPEN

    async def test_execute_fails_immediately_when_circuit_open(self):
        """Request fails immediately when circuit is OPEN."""
        config = CircuitBreakerConfig(recovery_timeout_s=10)
        manager = ConnectionManager(circuit_breaker_config=config)
        operation = AsyncMock()

        # Manually open circuit
        breaker = manager.get_circuit_breaker("example.com")
        breaker._open_circuit()

        with pytest.raises(ConnectionError, match="Circuit breaker is OPEN"):
            await manager.execute_with_retry("example.com", operation)

        # Operation should not have been called
        assert operation.call_count == 0

    async def test_backoff_delay_increases(self):
        """Backoff delay increases exponentially."""
        manager = ConnectionManager(
            RetryConfig(max_attempts=4, initial_delay_s=0.01, exponential_base=2.0)
        )
        operation = AsyncMock(side_effect=[Exception("f1"), Exception("f2"), Exception("f3"), "ok"])

        import time

        start = time.time()
        await manager.execute_with_retry("example.com", operation)
        elapsed = time.time() - start

        # Delays: 0.01, 0.02, 0.04 = 0.07s minimum (plus some overhead)
        assert elapsed >= 0.05  # Some tolerance for execution


class TestConnectionManagerRecovery:
    """Test recovery and circuit breaker integration."""

    @pytest.mark.asyncio
    async def test_recovery_from_half_open(self):
        """Successful operation in HALF_OPEN closes circuit."""
        config = CircuitBreakerConfig(failure_threshold=1, recovery_timeout_s=0, success_threshold=1)
        manager = ConnectionManager(circuit_breaker_config=config)
        operation = AsyncMock()

        # Fail to open circuit
        operation.side_effect = Exception("fail")
        with pytest.raises(ConnectionError):
            await manager.execute_with_retry("example.com", operation)

        breaker = manager.get_circuit_breaker("example.com")
        assert breaker.state == CircuitState.OPEN

        # Wait for timeout and test recovery
        import time

        time.sleep(0.01)

        operation.side_effect = None
        operation.return_value = "recovered"
        result = await manager.execute_with_retry("example.com", operation)

        assert result == "recovered"
        assert breaker.state == CircuitState.CLOSED

    def test_host_status(self):
        """Get host status."""
        manager = ConnectionManager()

        # Unknown host
        status = manager.get_host_status("unknown.com")
        assert status["status"] == "unknown"

        # Known host
        breaker = manager.get_circuit_breaker("example.com")
        status = manager.get_host_status("example.com")

        assert status["host"] == "example.com"
        assert status["status"] == "closed"
        assert status["failures"] == 0

    def test_reset_host(self):
        """Manually reset circuit breaker."""
        manager = ConnectionManager()
        breaker = manager.get_circuit_breaker("example.com")
        breaker._open_circuit()

        assert breaker.state == CircuitState.OPEN

        manager.reset_host("example.com")

        assert breaker.state == CircuitState.CLOSED

    def test_reset_all(self):
        """Reset all circuit breakers."""
        manager = ConnectionManager()

        breaker1 = manager.get_circuit_breaker("host1.com")
        breaker2 = manager.get_circuit_breaker("host2.com")

        breaker1._open_circuit()
        breaker2._open_circuit()

        assert breaker1.state == CircuitState.OPEN
        assert breaker2.state == CircuitState.OPEN

        manager.reset_all()

        assert breaker1.state == CircuitState.CLOSED
        assert breaker2.state == CircuitState.CLOSED


class TestConnectionManagerIntegration:
    """Integration tests for connection manager."""

    def test_multiple_hosts_independent(self):
        """Different hosts have independent circuit breakers."""
        manager = ConnectionManager()

        breaker1 = manager.get_circuit_breaker("host1.com")
        breaker2 = manager.get_circuit_breaker("host2.com")

        breaker1._open_circuit()

        assert breaker1.state == CircuitState.OPEN
        assert breaker2.state == CircuitState.CLOSED

    @pytest.mark.asyncio
    async def test_circuit_breaker_prevents_cascade_failure(self):
        """Circuit breaker prevents cascading failures."""
        config = CircuitBreakerConfig(failure_threshold=2, recovery_timeout_s=10)
        manager = ConnectionManager(circuit_breaker_config=config)

        operation = AsyncMock(side_effect=Exception("Service down"))

        # Fail twice to open circuit
        for i in range(2):
            with pytest.raises(ConnectionError):
                await manager.execute_with_retry("broken.com", operation)

        # Third attempt should fail immediately without hitting operation
        operation.reset_mock()
        with pytest.raises(ConnectionError, match="Circuit breaker is OPEN"):
            await manager.execute_with_retry("broken.com", operation)

        # Operation not called at all
        assert operation.call_count == 0
