"""Unit tests for HTTP client connection pooling (Story 2.6)."""

from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from rabbitmq_mcp_connection.http_client import HTTPClientManager, PoolMetrics


@pytest.fixture
def http_client_manager():
    """Create HTTPClientManager instance for testing."""
    return HTTPClientManager(
        host="localhost",
        port=15672,
        username="guest",
        password="guest",
        use_tls=False,
        pool_size=5,
        pool_health_check_interval=30,
    )


class TestPoolConfiguration:
    """Test connection pool configuration parameters (AC #1-4)."""

    def test_pool_size_from_settings(self):
        """Test pool size configured from max_http_connections parameter."""
        client = HTTPClientManager(
            host="localhost",
            port=15672,
            username="guest",
            password="guest",
            pool_size=10,
        )
        assert client.pool_size == 10

    def test_default_pool_size(self):
        """Test default pool size is 5."""
        client = HTTPClientManager(
            host="localhost",
            port=15672,
            username="guest",
            password="guest",
        )
        assert client.pool_size == 5

    def test_pool_limits_configured(self):
        """Test httpx.Limits configured with max_connections and keepalive_expiry."""
        client = HTTPClientManager(
            host="localhost",
            port=15672,
            username="guest",
            password="guest",
            pool_size=8,
        )
        # Verify pool size is stored correctly
        assert client.pool_size == 8
        # Note: httpx internal attributes are private and not part of public API
        # Testing via behavior in integration tests instead

    def test_pool_timeout_configured(self):
        """Test pool block timeout is 10 seconds (AC #2)."""
        client = HTTPClientManager(
            host="localhost",
            port=15672,
            username="guest",
            password="guest",
        )
        # DEFAULT_POOL_TIMEOUT should be 10
        assert client.client._timeout.pool == 10.0


class TestPoolMetricsTracking:
    """Test pool metrics tracking (AC #5, #11)."""

    def test_get_pool_status_returns_metrics(self, http_client_manager):
        """Test get_pool_status() returns PoolMetrics with all fields."""
        status = http_client_manager.get_pool_status()

        assert isinstance(status, PoolMetrics)
        assert status.pool_size == 5
        assert status.active_connections == 0
        assert status.total_requests == 0
        assert status.successful_requests == 0
        assert status.failed_requests == 0
        assert status.evictions_last_hour == 0
        assert status.pool_exhaustion_count == 0
        assert status.health_score == 1.0

    @pytest.mark.asyncio
    async def test_successful_request_updates_metrics(self, http_client_manager):
        """Test successful requests update metrics correctly."""
        with patch.object(
            http_client_manager.client, "request", new_callable=AsyncMock
        ) as mock_request:
            mock_response = MagicMock()
            mock_response.status_code = 200
            mock_request.return_value = mock_response

            await http_client_manager.request("GET", "/overview")

            status = http_client_manager.get_pool_status()
            assert status.total_requests == 1
            assert status.successful_requests == 1
            assert status.failed_requests == 0
            assert status.health_score == 1.0

    @pytest.mark.asyncio
    async def test_failed_request_updates_metrics(self, http_client_manager):
        """Test failed requests update metrics correctly."""
        with patch.object(
            http_client_manager.client, "request", new_callable=AsyncMock
        ) as mock_request:
            mock_request.side_effect = httpx.TimeoutException("Request timeout")

            with pytest.raises(httpx.TimeoutException):
                await http_client_manager.request("GET", "/overview")

            status = http_client_manager.get_pool_status()
            assert status.total_requests == 1
            assert status.successful_requests == 0
            assert status.failed_requests == 1
            assert status.health_score == 0.0

    @pytest.mark.asyncio
    async def test_health_score_calculation(self, http_client_manager):
        """Test health score calculation from request history."""
        with patch.object(
            http_client_manager, "_check_health_score", new_callable=AsyncMock
        ):
            with patch.object(
                http_client_manager, "_evict_stale_connections", new_callable=AsyncMock
            ):
                with patch.object(
                    http_client_manager.client, "request", new_callable=AsyncMock
                ) as mock_request:
                    # Simulate 8 successful + 2 failed requests (health_score = 0.8)
                    mock_response = MagicMock()
                    mock_response.status_code = 200

                    for _ in range(8):
                        mock_request.return_value = mock_response
                        await http_client_manager.request("GET", "/overview")

                    for _ in range(2):
                        mock_request.side_effect = httpx.TimeoutException("Timeout")
                        with pytest.raises(httpx.TimeoutException):
                            await http_client_manager.request("GET", "/overview")
                        mock_request.side_effect = None

                    status = http_client_manager.get_pool_status()
                    assert status.total_requests == 10
                    assert status.successful_requests == 8
                    assert status.failed_requests == 2
                    assert status.health_score == 0.8


class TestStaleConnectionEviction:
    """Test stale connection detection and eviction (AC #8)."""

    @pytest.mark.asyncio
    async def test_http_503_triggers_eviction(self, http_client_manager):
        """Test HTTP 503 Service Unavailable triggers pool eviction."""
        with patch.object(
            http_client_manager.client, "request", new_callable=AsyncMock
        ) as mock_request:
            mock_response = MagicMock()
            mock_response.status_code = 503
            mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
                "Service Unavailable", request=MagicMock(), response=mock_response
            )
            mock_request.return_value = mock_response

            with patch.object(
                http_client_manager, "_evict_stale_connections", new_callable=AsyncMock
            ) as mock_evict:
                with pytest.raises(httpx.HTTPStatusError):
                    await http_client_manager.request("GET", "/overview")

                mock_evict.assert_called_once_with("HTTP 503 Service Unavailable")

    @pytest.mark.asyncio
    async def test_timeout_triggers_eviction(self, http_client_manager):
        """Test timeout exception triggers pool eviction."""
        with patch.object(
            http_client_manager.client, "request", new_callable=AsyncMock
        ) as mock_request:
            mock_request.side_effect = httpx.TimeoutException("Request timeout")

            with patch.object(
                http_client_manager, "_evict_stale_connections", new_callable=AsyncMock
            ) as mock_evict:
                with pytest.raises(httpx.TimeoutException):
                    await http_client_manager.request("GET", "/overview")

                assert mock_evict.called
                assert "Timeout" in str(mock_evict.call_args)

    @pytest.mark.asyncio
    async def test_connect_error_triggers_eviction(self, http_client_manager):
        """Test socket connection error triggers pool eviction."""
        with patch.object(
            http_client_manager.client, "request", new_callable=AsyncMock
        ) as mock_request:
            mock_request.side_effect = httpx.ConnectError("Connection refused")

            with patch.object(
                http_client_manager, "_evict_stale_connections", new_callable=AsyncMock
            ) as mock_evict:
                with pytest.raises(httpx.ConnectError):
                    await http_client_manager.request("GET", "/overview")

                assert mock_evict.called
                assert "Socket error" in str(mock_evict.call_args)

    @pytest.mark.asyncio
    async def test_ssl_error_triggers_eviction(self, http_client_manager):
        """Test SSL/TLS error triggers pool eviction."""
        with patch.object(
            http_client_manager.client, "request", new_callable=AsyncMock
        ) as mock_request:
            mock_request.side_effect = httpx.TransportError(
                "SSL: CERTIFICATE_VERIFY_FAILED"
            )

            with patch.object(
                http_client_manager, "_evict_stale_connections", new_callable=AsyncMock
            ) as mock_evict:
                with pytest.raises(httpx.TransportError):
                    await http_client_manager.request("GET", "/overview")

                assert mock_evict.called
                assert "SSL/TLS error" in str(mock_evict.call_args)

    @pytest.mark.asyncio
    async def test_eviction_increments_counter(self, http_client_manager):
        """Test eviction increments evictions counter."""
        initial_count = http_client_manager._evictions_count

        await http_client_manager._evict_stale_connections("Test eviction")

        assert http_client_manager._evictions_count == initial_count + 1

        status = http_client_manager.get_pool_status()
        assert status.evictions_last_hour == 1


class TestPoolExhaustionWarning:
    """Test pool exhaustion warning (AC #6)."""

    @pytest.mark.asyncio
    async def test_pool_timeout_logs_warning(self, http_client_manager):
        """Test httpx.PoolTimeout triggers pool exhaustion warning."""
        with patch.object(
            http_client_manager.client, "request", new_callable=AsyncMock
        ) as mock_request:
            mock_request.side_effect = httpx.PoolTimeout("Pool exhausted")

            with pytest.raises(httpx.PoolTimeout):
                await http_client_manager.request("GET", "/overview")

            status = http_client_manager.get_pool_status()
            assert status.pool_exhaustion_count == 1

    @pytest.mark.asyncio
    async def test_pool_exhaustion_count_increments(self, http_client_manager):
        """Test pool_exhaustion_count increments on each pool timeout."""
        with patch.object(
            http_client_manager.client, "request", new_callable=AsyncMock
        ) as mock_request:
            mock_request.side_effect = httpx.PoolTimeout("Pool exhausted")

            for _ in range(3):
                with pytest.raises(httpx.PoolTimeout):
                    await http_client_manager.request("GET", "/overview")

            status = http_client_manager.get_pool_status()
            assert status.pool_exhaustion_count == 3


class TestConfigurationChangeDetection:
    """Test configuration change detection and pool flush (AC #9)."""

    @pytest.mark.asyncio
    async def test_config_change_detected(self, http_client_manager):
        """Test configuration change is detected and pool is flushed."""
        changed = await http_client_manager.reload_on_config_change(
            host="newhost",
            port=15672,
            username="guest",
            password="guest",
            use_tls=False,
            verify_cert=True,
            ca_bundle=None,
        )

        assert changed is True
        assert "newhost" in http_client_manager.base_url

    @pytest.mark.asyncio
    async def test_no_change_not_detected(self, http_client_manager):
        """Test no change returns False without flushing pool."""
        changed = await http_client_manager.reload_on_config_change(
            host="localhost",
            port=15672,
            username="guest",
            password="guest",
            use_tls=False,
            verify_cert=True,
            ca_bundle=None,
        )

        assert changed is False


class TestHealthScoreAlerting:
    """Test pool health score alerting (AC #11)."""

    @pytest.mark.asyncio
    async def test_health_score_below_90_logs_warning(self, http_client_manager):
        """Test health score < 0.90 logs warning."""
        # Simulate 85% success rate
        with patch.object(
            http_client_manager, "_check_health_score", new_callable=AsyncMock
        ):
            with patch.object(
                http_client_manager, "_evict_stale_connections", new_callable=AsyncMock
            ):
                with patch.object(
                    http_client_manager.client, "request", new_callable=AsyncMock
                ) as mock_request:
                    mock_response = MagicMock()
                    mock_response.status_code = 200

                    for _ in range(85):
                        mock_request.return_value = mock_response
                        await http_client_manager.request("GET", "/overview")

                    for _ in range(15):
                        mock_request.side_effect = httpx.TimeoutException("Timeout")
                        with pytest.raises(httpx.TimeoutException):
                            await http_client_manager.request("GET", "/overview")
                        mock_request.side_effect = None

                    status = http_client_manager.get_pool_status()
                    assert status.health_score == 0.85

    @pytest.mark.asyncio
    async def test_health_score_below_75_triggers_reconnection(
        self, http_client_manager
    ):
        """Test health score < 0.75 triggers reconnection."""
        mock_reconnection_manager = MagicMock()
        mock_reconnection_manager.handle_connection_loss = AsyncMock()
        http_client_manager._reconnection_manager = mock_reconnection_manager

        # Simulate 70% success rate
        with patch.object(
            http_client_manager, "_check_health_score", new_callable=AsyncMock
        ):
            with patch.object(
                http_client_manager, "_evict_stale_connections", new_callable=AsyncMock
            ):
                with patch.object(
                    http_client_manager.client, "request", new_callable=AsyncMock
                ) as mock_request:
                    mock_response = MagicMock()
                    mock_response.status_code = 200

                    for _ in range(70):
                        mock_request.return_value = mock_response
                        await http_client_manager.request("GET", "/overview")

                    for _ in range(30):
                        mock_request.side_effect = httpx.TimeoutException("Timeout")
                        with pytest.raises(httpx.TimeoutException):
                            await http_client_manager.request("GET", "/overview")
                        mock_request.side_effect = None

                    status = http_client_manager.get_pool_status()
                    assert status.health_score == 0.70
