"""
Test suite for configurable rate limit settings.

Ensures that GEMINI_RPM_LIMIT environment variable correctly configures
the rate limiter for different API tiers (free, paid tier 1, tier 2).

These tests deliberately avoid ``importlib.reload(src.config)`` for the
simple "env-var-to-field" assertions: a reload swaps the module-level
``src.config.config`` for a brand-new Settings instance while every
already-imported production module (``src.persistence``,
``src.agents.research_nodes``, …) still holds the OLD reference. Tests
that run later and ``monkeypatch.setattr(config, …)`` then mutate a
different object than the one production reads — which silently breaks
unrelated tests in the full suite (May 2026 cross-test leakage incident).

Use ``Settings()`` to construct a fresh instance against a patched
environment instead. The two reload-mechanics tests at the bottom of
this file are guarded by a fixture that restores ``src.config.config``
to the original singleton object on teardown.
"""

import importlib
import os
from unittest.mock import patch

import pytest


@pytest.fixture
def restore_config_singleton():
    """Restore ``src.config.config`` to the original singleton object on
    teardown. Use this around any test that calls
    ``importlib.reload(src.config)`` so the reload doesn't orphan
    references already held by production modules.
    """
    import src.config

    saved = src.config.config
    yield
    src.config.config = saved


class TestRateLimitConfiguration:
    """Test rate limit configuration from environment variables."""

    def test_default_free_tier_15_rpm(self, monkeypatch):
        """Test that default rate limit is 15 RPM (free tier) when no env var is set."""
        from src.config import Settings

        monkeypatch.delenv("GEMINI_RPM_LIMIT", raising=False)
        # Skip if a local .env still drives a non-default value (user may
        # be on paid tier even without the env var explicitly set).
        if Settings().gemini_rpm_limit != 15:
            pytest.skip(
                "GEMINI_RPM_LIMIT default in this environment is non-15; "
                "skipping free-tier default test"
            )
        assert Settings().gemini_rpm_limit == 15

    def test_paid_tier_1_360_rpm(self, monkeypatch):
        """Test setting rate limit to 360 RPM (paid tier 1)."""
        from src.config import Settings

        monkeypatch.setenv("GEMINI_RPM_LIMIT", "360")
        assert Settings().gemini_rpm_limit == 360

    def test_paid_tier_2_1000_rpm(self, monkeypatch):
        """Test setting rate limit to 1000 RPM (paid tier 2)."""
        from src.config import Settings

        monkeypatch.setenv("GEMINI_RPM_LIMIT", "1000")
        assert Settings().gemini_rpm_limit == 1000

    def test_custom_rpm_limit(self, monkeypatch):
        """Test setting custom RPM limit."""
        from src.config import Settings

        monkeypatch.setenv("GEMINI_RPM_LIMIT", "500")
        assert Settings().gemini_rpm_limit == 500


class TestRateLimiterCreation:
    """Test rate limiter creation from RPM settings."""

    def test_rate_limiter_15_rpm_calculation(self):
        """Test rate limiter RPS calculation for 15 RPM (free tier)."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(15)

        # 15 RPM with 80% safety factor:
        # RPS = (15 / 60) * 0.8 = 0.2 RPS
        expected_rps = (15 / 60.0) * 0.8
        assert limiter.requests_per_second == pytest.approx(expected_rps, abs=0.01)

        # Bucket size: max(5, int(15 * 0.1)) = max(5, 1) = 5
        assert limiter.max_bucket_size == 5

    def test_rate_limiter_360_rpm_calculation(self):
        """Test rate limiter RPS calculation for 360 RPM (paid tier 1)."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(360)

        # 360 RPM with 80% safety factor:
        # RPS = (360 / 60) * 0.8 = 4.8 RPS
        expected_rps = (360 / 60.0) * 0.8
        assert limiter.requests_per_second == pytest.approx(expected_rps, abs=0.01)

        # Bucket size: max(5, int(360 * 0.1)) = max(5, 36) = 36
        assert limiter.max_bucket_size == 36

    def test_rate_limiter_1000_rpm_calculation(self):
        """Test rate limiter RPS calculation for 1000 RPM (paid tier 2)."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(1000)

        # 1000 RPM with 80% safety factor:
        # RPS = (1000 / 60) * 0.8 = 13.33 RPS
        expected_rps = (1000 / 60.0) * 0.8
        assert limiter.requests_per_second == pytest.approx(expected_rps, abs=0.01)

        # Bucket size: max(5, int(1000 * 0.1)) = max(5, 100) = 100
        assert limiter.max_bucket_size == 100

    def test_rate_limiter_minimum_bucket_size(self):
        """Test that bucket size has minimum of 5."""
        from src.llms import _create_rate_limiter_from_rpm

        # Very low RPM (2 RPM)
        limiter = _create_rate_limiter_from_rpm(2)

        # Bucket size: max(5, int(2 * 0.1)) = max(5, 0) = 5
        assert limiter.max_bucket_size == 5


class TestSafetyMargin:
    """Test 20% safety margin in rate calculations."""

    def test_15_rpm_uses_80_percent(self):
        """Test that 15 RPM uses 80% safety margin (12 effective RPM)."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(15)

        # Effective RPS: (15 * 0.8) / 60 = 12 / 60 = 0.2
        # This leaves 20% buffer to avoid hitting the actual 15 RPM limit
        effective_rpm = limiter.requests_per_second * 60
        assert effective_rpm == pytest.approx(12.0, abs=0.1)

    def test_360_rpm_uses_80_percent(self):
        """Test that 360 RPM uses 80% safety margin (288 effective RPM)."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(360)

        # Effective RPS: (360 * 0.8) / 60 = 288 / 60 = 4.8
        effective_rpm = limiter.requests_per_second * 60
        assert effective_rpm == pytest.approx(288.0, abs=0.1)

    def test_1000_rpm_uses_80_percent(self):
        """Test that 1000 RPM uses 80% safety margin (800 effective RPM)."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(1000)

        # Effective RPS: (1000 * 0.8) / 60 = 800 / 60 = 13.33
        effective_rpm = limiter.requests_per_second * 60
        assert effective_rpm == pytest.approx(800.0, abs=0.1)


class TestGlobalRateLimiterInitialization:
    """Test that global rate limiter is initialized correctly."""

    def test_global_rate_limiter_uses_config(self, restore_config_singleton):
        """Test that GLOBAL_RATE_LIMITER uses config.gemini_rpm_limit.

        Uses ``restore_config_singleton`` to re-alias ``src.config.config``
        back to the original singleton on teardown — otherwise the
        reload here orphans references held by production modules
        already imported, breaking unrelated tests downstream.
        """
        os.environ["GEMINI_RPM_LIMIT"] = "100"

        # Reimport to pick up new config
        import src.config
        import src.llms

        importlib.reload(src.config)
        importlib.reload(src.llms)

        # Global rate limiter should use 100 RPM
        # RPS = (100 / 60) * 0.8 = 1.33 RPS
        expected_rps = (100 / 60.0) * 0.8
        assert src.llms.GLOBAL_RATE_LIMITER.requests_per_second == pytest.approx(
            expected_rps, abs=0.01
        )

        # Clean up env; the fixture re-aliases src.config.config and the
        # finally-block resets the limiter cache.
        del os.environ["GEMINI_RPM_LIMIT"]
        importlib.reload(src.llms)

    def test_global_rate_limiter_uses_live_config_after_reload_under_patch(
        self, monkeypatch, restore_config_singleton
    ):
        """Reloading llms under a patched config must not freeze a stale mock."""
        import src.config
        import src.llms

        original_rpm = src.config.config.gemini_rpm_limit

        try:
            with patch("src.config.config") as mock_config:
                mock_config.gemini_rpm_limit = 999
                importlib.reload(src.llms)

            src.llms.GLOBAL_RATE_LIMITER._instance = None
            monkeypatch.setattr(src.config.config, "gemini_rpm_limit", 120)

            expected_rps = (120 / 60.0) * 0.8
            assert src.llms.GLOBAL_RATE_LIMITER.requests_per_second == pytest.approx(
                expected_rps, abs=0.01
            )
        finally:
            src.llms.GLOBAL_RATE_LIMITER._instance = None
            src.config.config.gemini_rpm_limit = original_rpm
            importlib.reload(src.llms)


class TestTierComparison:
    """Test performance differences between tiers."""

    def test_paid_tier_1_is_24x_faster_than_free(self):
        """Verify that paid tier 1 (360 RPM) is 24x faster than free (15 RPM)."""
        from src.llms import _create_rate_limiter_from_rpm

        free_limiter = _create_rate_limiter_from_rpm(15)
        paid1_limiter = _create_rate_limiter_from_rpm(360)

        # Ratio should be 360/15 = 24
        ratio = paid1_limiter.requests_per_second / free_limiter.requests_per_second
        assert ratio == pytest.approx(24.0, abs=0.1)

    def test_paid_tier_2_is_67x_faster_than_free(self):
        """Verify that paid tier 2 (1000 RPM) is ~67x faster than free (15 RPM)."""
        from src.llms import _create_rate_limiter_from_rpm

        free_limiter = _create_rate_limiter_from_rpm(15)
        paid2_limiter = _create_rate_limiter_from_rpm(1000)

        # Ratio should be 1000/15 = 66.67
        ratio = paid2_limiter.requests_per_second / free_limiter.requests_per_second
        assert ratio == pytest.approx(66.67, abs=0.1)

    def test_paid_tier_2_is_2_8x_faster_than_tier_1(self):
        """Verify that paid tier 2 (1000 RPM) is ~2.8x faster than tier 1 (360 RPM)."""
        from src.llms import _create_rate_limiter_from_rpm

        paid1_limiter = _create_rate_limiter_from_rpm(360)
        paid2_limiter = _create_rate_limiter_from_rpm(1000)

        # Ratio should be 1000/360 = 2.78
        ratio = paid2_limiter.requests_per_second / paid1_limiter.requests_per_second
        assert ratio == pytest.approx(2.78, abs=0.1)


class TestEdgeCases:
    """Test edge cases in rate limit configuration."""

    def test_very_low_rpm(self):
        """Test that very low RPM values work correctly."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(1)

        # 1 RPM with 80% safety factor:
        # RPS = (1 / 60) * 0.8 = 0.0133 RPS
        expected_rps = (1 / 60.0) * 0.8
        assert limiter.requests_per_second == pytest.approx(expected_rps, abs=0.001)

        # Minimum bucket size
        assert limiter.max_bucket_size == 5

    def test_very_high_rpm(self):
        """Test that very high RPM values work correctly."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(10000)

        # 10000 RPM with 80% safety factor:
        # RPS = (10000 / 60) * 0.8 = 133.33 RPS
        expected_rps = (10000 / 60.0) * 0.8
        assert limiter.requests_per_second == pytest.approx(expected_rps, abs=0.1)

        # Bucket size: max(5, int(10000 * 0.1)) = 1000
        assert limiter.max_bucket_size == 1000

    def test_invalid_rpm_string_raises_error(self):
        """Test that invalid RPM string in env var raises error."""
        os.environ["GEMINI_RPM_LIMIT"] = "invalid"

        with pytest.raises(ValueError):
            import src.config

            importlib.reload(src.config)

        # Clean up
        del os.environ["GEMINI_RPM_LIMIT"]


class TestLogging:
    """Test that rate limiter initialization produces correct configuration."""

    def test_rate_limiter_configuration_values(self):
        """Test that rate limiter creation produces correct configuration values."""
        from src.llms import _create_rate_limiter_from_rpm

        limiter = _create_rate_limiter_from_rpm(360)

        # Verify the limiter was configured correctly (this is what logging would show)
        # 360 RPM with 80% safety factor = 4.8 RPS
        expected_rps = (360 / 60.0) * 0.8
        assert limiter.requests_per_second == pytest.approx(expected_rps, abs=0.01)
        assert limiter.max_bucket_size == 36

    def test_different_tiers_produce_different_configurations(self):
        """Test that different tier configurations produce different rate limiters."""
        from src.llms import _create_rate_limiter_from_rpm

        # Free tier
        free_limiter = _create_rate_limiter_from_rpm(15)
        assert free_limiter.requests_per_second == pytest.approx(0.2, abs=0.01)
        assert free_limiter.max_bucket_size == 5

        # Paid tier 1
        paid1_limiter = _create_rate_limiter_from_rpm(360)
        assert paid1_limiter.requests_per_second == pytest.approx(4.8, abs=0.01)
        assert paid1_limiter.max_bucket_size == 36

        # Paid tier 2
        paid2_limiter = _create_rate_limiter_from_rpm(1000)
        assert paid2_limiter.requests_per_second == pytest.approx(13.33, abs=0.01)
        assert paid2_limiter.max_bucket_size == 100


if __name__ == "__main__":
    pytest.main([__file__, "-v", "-s"])
