"""Tests for sliding window rate limiter."""

import pytest
import time
from unittest.mock import patch

from skop_runner.rate_limiter import SlidingWindowRateLimiter


class TestSlidingWindowRateLimiter:
    """Test rate limiter behavior."""

    def test_allows_under_limit(self):
        """Messages under the rate limit should be allowed."""
        limiter = SlidingWindowRateLimiter(max_per_second=10)
        for _ in range(10):
            assert limiter.allow() is True

    def test_rejects_over_limit(self):
        """Messages exceeding the rate limit should be rejected."""
        limiter = SlidingWindowRateLimiter(max_per_second=5)
        for _ in range(5):
            assert limiter.allow() is True
        assert limiter.allow() is False

    def test_window_expires(self):
        """Old timestamps outside the window should be evicted."""
        limiter = SlidingWindowRateLimiter(max_per_second=2, window_seconds=0.1)
        assert limiter.allow() is True
        assert limiter.allow() is True
        assert limiter.allow() is False

        # Wait for the window to expire
        time.sleep(0.15)
        assert limiter.allow() is True

    def test_warns_once_per_burst(self):
        """Warning should be logged once on first rejection, not on subsequent ones."""
        limiter = SlidingWindowRateLimiter(max_per_second=2, label="test")

        with patch("skop_runner.rate_limiter.logger") as mock_logger:
            limiter.allow()
            limiter.allow()
            limiter.allow()  # First rejection — should warn
            limiter.allow()  # Second rejection — should NOT warn

            assert mock_logger.warning.call_count == 1
            assert "test" in mock_logger.warning.call_args[0][0]

    def test_warning_resets_after_allow(self):
        """Warning flag should reset when a message is allowed after a burst."""
        limiter = SlidingWindowRateLimiter(max_per_second=2, window_seconds=0.1)

        with patch("skop_runner.rate_limiter.logger") as mock_logger:
            limiter.allow()
            limiter.allow()
            limiter.allow()  # Rejected — warns
            assert mock_logger.warning.call_count == 1

            # Wait for window to expire
            time.sleep(0.15)
            limiter.allow()  # Allowed — resets warning
            limiter.allow()
            limiter.allow()  # Rejected again — should warn again
            assert mock_logger.warning.call_count == 2

    def test_reset_clears_state(self):
        """reset() should clear all timestamps and the warning flag."""
        limiter = SlidingWindowRateLimiter(max_per_second=2)
        limiter.allow()
        limiter.allow()
        assert limiter.allow() is False  # At limit

        limiter.reset()
        assert limiter.allow() is True  # Fresh start

    def test_label_in_warning(self):
        """Warning message should include the label."""
        limiter = SlidingWindowRateLimiter(max_per_second=1, label="outgoing notifications")

        with patch("skop_runner.rate_limiter.logger") as mock_logger:
            limiter.allow()
            limiter.allow()  # Rejected

            warning_msg = mock_logger.warning.call_args[0][0]
            assert "outgoing notifications" in warning_msg
            assert "1/s" in warning_msg

    def test_no_label(self):
        """Warning should work without a label."""
        limiter = SlidingWindowRateLimiter(max_per_second=1)

        with patch("skop_runner.rate_limiter.logger") as mock_logger:
            limiter.allow()
            limiter.allow()  # Rejected

            warning_msg = mock_logger.warning.call_args[0][0]
            assert "1/s" in warning_msg
