"""
Tests for database connection pool monitoring.

Verifies that the PoolMonitor correctly:
- Reports pool statistics (checked in/out, overflow)
- Classifies health status (healthy/warning/critical)
- Tracks history of stats over time
- Logs warnings when usage exceeds threshold

Author: Claude
Date: 2025-11-27
"""

import pytest
from unittest.mock import MagicMock, patch

from app.monitoring.pool_monitor import PoolMonitor, PoolStats


# ============================================================================
# TEST FIXTURES
# ============================================================================


@pytest.fixture
def mock_engine():
    """
    Create a mock SQLAlchemy engine with pool stats.

    Default configuration: 15 available, 5 in use, no overflow.
    """
    engine = MagicMock()
    pool = MagicMock()
    pool.checkedin.return_value = 15
    pool.checkedout.return_value = 5
    pool.overflow.return_value = 0
    engine.pool = pool
    return engine


@pytest.fixture
def mock_settings():
    """Mock settings with pool configuration."""
    settings = MagicMock()
    settings.db_pool_size = 20
    settings.db_max_overflow = 10
    return settings


# ============================================================================
# POOL STATS TESTS
# ============================================================================


class TestPoolMonitorStats:
    """Tests for PoolMonitor.get_stats()."""

    def test_get_stats_returns_pool_stats(self, mock_engine, mock_settings):
        """
        Verify get_stats() returns PoolStats with correct values.

        The returned PoolStats should reflect the current pool state
        from the engine's pool attribute.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)
            stats = monitor.get_stats()

            assert isinstance(stats, PoolStats)
            assert stats.checkedout == 5
            assert stats.checkedin == 15
            assert stats.overflow == 0
            assert stats.pool_size == 20
            assert stats.max_overflow == 10
            assert stats.total_capacity == 30

    def test_get_stats_calculates_usage_percent(self, mock_engine, mock_settings):
        """
        Verify usage percentage is calculated correctly.

        Usage = checkedout / total_capacity
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)
            stats = monitor.get_stats()

            # 5 out of 30 = 16.67%
            assert stats.usage_percent == pytest.approx(5 / 30, rel=0.01)

    def test_get_stats_handles_zero_capacity(self, mock_engine):
        """
        Verify get_stats() handles zero capacity without division error.

        Edge case: if both pool_size and max_overflow are 0.
        """
        settings = MagicMock()
        settings.db_pool_size = 0
        settings.db_max_overflow = 0

        with patch("app.monitoring.pool_monitor.get_settings", return_value=settings):
            monitor = PoolMonitor(mock_engine)
            stats = monitor.get_stats()

            assert stats.usage_percent == 0
            assert stats.total_capacity == 0

    def test_get_stats_records_history(self, mock_engine, mock_settings):
        """
        Verify get_stats() records each call in history.

        History is used for monitoring dashboards and alerts.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)

            # Make multiple calls
            for _ in range(5):
                monitor.get_stats()

            history = monitor.get_history(limit=10)
            assert len(history) == 5

    def test_get_stats_limits_history_size(self, mock_engine, mock_settings):
        """
        Verify history is limited to prevent memory growth.

        Older entries should be removed when max_history is exceeded.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine, max_history=5)

            # Make more calls than max_history
            for _ in range(10):
                monitor.get_stats()

            history = monitor.get_history(limit=10)
            assert len(history) == 5


# ============================================================================
# HEALTH STATUS TESTS
# ============================================================================


class TestPoolMonitorHealth:
    """Tests for PoolMonitor.get_health_status()."""

    def test_health_status_healthy(self, mock_engine, mock_settings):
        """
        Verify health status is 'healthy' when usage < 75%.

        At 5/30 (16.7%) usage, status should be healthy.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)
            health = monitor.get_health_status()

            assert health["status"] == "healthy"
            assert health["in_use"] == 5
            assert health["available"] == 15

    def test_health_status_warning(self, mock_engine, mock_settings):
        """
        Verify health status is 'warning' when usage is 75-90%.

        At 24/30 (80%) usage, status should be warning.
        """
        mock_engine.pool.checkedout.return_value = 24
        mock_engine.pool.checkedin.return_value = 6

        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)
            health = monitor.get_health_status()

            assert health["status"] == "warning"
            assert health["usage_percent"] == pytest.approx(80, rel=1)

    def test_health_status_critical(self, mock_engine, mock_settings):
        """
        Verify health status is 'critical' when usage >= 90%.

        At 28/30 (93%) usage, status should be critical.
        """
        mock_engine.pool.checkedout.return_value = 28
        mock_engine.pool.checkedin.return_value = 2

        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)
            health = monitor.get_health_status()

            assert health["status"] == "critical"
            assert health["usage_percent"] >= 90

    def test_health_status_includes_timestamp(self, mock_engine, mock_settings):
        """
        Verify health status includes ISO timestamp.

        Timestamp is used for monitoring and alerting correlation.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)
            health = monitor.get_health_status()

            assert "timestamp" in health
            assert isinstance(health["timestamp"], str)
            assert "T" in health["timestamp"]  # ISO format

    def test_health_status_includes_all_fields(self, mock_engine, mock_settings):
        """
        Verify health status includes all required fields.

        All fields are needed for complete monitoring visibility.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)
            health = monitor.get_health_status()

            required_fields = [
                "status",
                "pool_size",
                "max_overflow",
                "available",
                "in_use",
                "overflow_active",
                "total_capacity",
                "usage_percent",
                "timestamp",
            ]

            for field in required_fields:
                assert field in health, f"Missing field: {field}"


# ============================================================================
# HISTORY TESTS
# ============================================================================


class TestPoolMonitorHistory:
    """Tests for PoolMonitor.get_history()."""

    def test_get_history_returns_recent_stats(self, mock_engine, mock_settings):
        """
        Verify get_history() returns recent stat snapshots.

        History should include checkedout, usage_percent, and timestamp.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)

            # Generate some history
            for _ in range(3):
                monitor.get_stats()

            history = monitor.get_history(limit=5)

            assert len(history) == 3
            for entry in history:
                assert "checkedout" in entry
                assert "usage_percent" in entry
                assert "timestamp" in entry

    def test_get_history_respects_limit(self, mock_engine, mock_settings):
        """
        Verify get_history() respects the limit parameter.

        Should return at most 'limit' entries.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)

            for _ in range(10):
                monitor.get_stats()

            history = monitor.get_history(limit=3)
            assert len(history) == 3

    def test_get_history_returns_empty_when_no_stats(self, mock_engine, mock_settings):
        """
        Verify get_history() returns empty list when no stats collected.

        Fresh monitor should have no history.
        """
        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)
            history = monitor.get_history()

            assert history == []


# ============================================================================
# ALERT THRESHOLD TESTS
# ============================================================================


class TestPoolMonitorAlerts:
    """Tests for alert threshold behavior."""

    def test_logs_warning_when_threshold_exceeded(self, mock_engine, mock_settings, caplog):
        """
        Verify warning is logged when usage exceeds alert threshold.

        Default threshold is 80%. At 85% usage, warning should be logged.
        """
        mock_engine.pool.checkedout.return_value = 26  # 86.7%
        mock_engine.pool.checkedin.return_value = 4

        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine, alert_threshold=0.8)

            import logging
            with caplog.at_level(logging.WARNING):
                monitor.get_stats()

            assert "Connection pool usage high" in caplog.text

    def test_logs_info_when_overflow_active(self, mock_engine, mock_settings, caplog):
        """
        Verify info is logged when overflow connections are in use.

        Overflow indicates pool is at capacity and using extra connections.
        """
        mock_engine.pool.overflow.return_value = 3

        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine)

            import logging
            with caplog.at_level(logging.INFO):
                monitor.get_stats()

            assert "Pool overflow active" in caplog.text

    def test_no_warning_when_below_threshold(self, mock_engine, mock_settings, caplog):
        """
        Verify no warning when usage is below threshold.

        At 50% usage with 80% threshold, no warning should be logged.
        """
        mock_engine.pool.checkedout.return_value = 10  # 33%
        mock_engine.pool.checkedin.return_value = 20

        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = PoolMonitor(mock_engine, alert_threshold=0.8)

            import logging
            with caplog.at_level(logging.WARNING):
                monitor.get_stats()

            assert "Connection pool usage high" not in caplog.text


# ============================================================================
# INIT FUNCTION TESTS
# ============================================================================


class TestInitPoolMonitor:
    """Tests for init_pool_monitor function."""

    def test_init_pool_monitor_creates_global_instance(self, mock_engine, mock_settings):
        """
        Verify init_pool_monitor() creates global instance.

        The global instance is used by health endpoints.
        """
        from app.monitoring.pool_monitor import init_pool_monitor, pool_monitor

        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = init_pool_monitor(mock_engine)

            assert monitor is not None
            assert isinstance(monitor, PoolMonitor)

    def test_init_pool_monitor_returns_monitor(self, mock_engine, mock_settings):
        """
        Verify init_pool_monitor() returns the created monitor.

        Return value allows caller to use the monitor directly.
        """
        from app.monitoring.pool_monitor import init_pool_monitor

        with patch("app.monitoring.pool_monitor.get_settings", return_value=mock_settings):
            monitor = init_pool_monitor(mock_engine)

            # Should be able to use the returned monitor
            stats = monitor.get_stats()
            assert stats is not None
