"""
Unit tests for platform/api/throttling.py

Tests the GlobalBurstRateThrottle, RateLimitHeadersMixin, and helper functions.
"""

import time
from unittest.mock import Mock, patch, MagicMock

import pytest

from smoothschedule.platform.api.throttling import (
    GlobalBurstRateThrottle,
    RateLimitHeadersMixin,
    get_throttle_response_data,
)


class TestGlobalBurstRateThrottle:
    """Tests for GlobalBurstRateThrottle class."""

    def test_init_sets_defaults(self):
        """Should initialize with empty history and None values."""
        throttle = GlobalBurstRateThrottle()

        assert throttle.history == {}
        assert throttle.now is None
        assert throttle.token is None

    def test_allow_request_returns_true_when_no_api_token(self):
        """Should allow request when no API token is present."""
        throttle = GlobalBurstRateThrottle()
        request = Mock()
        request.api_token = None
        view = Mock()

        result = throttle.allow_request(request, view)

        assert result is True

    def test_allow_request_returns_true_when_api_token_attr_missing(self):
        """Should allow request when api_token attribute doesn't exist."""
        throttle = GlobalBurstRateThrottle()
        request = Mock(spec=[])  # No api_token attribute
        view = Mock()

        result = throttle.allow_request(request, view)

        assert result is True

    @patch('smoothschedule.platform.api.throttling.cache')
    @patch('smoothschedule.platform.api.throttling.time')
    def test_allow_request_with_token_under_limit(self, mock_time, mock_cache):
        """Should allow request when under both hourly and minute limits."""
        mock_time.time.return_value = 1000.0
        mock_cache.get.return_value = 10  # Well under limit
        mock_cache.incr.return_value = 11

        throttle = GlobalBurstRateThrottle()
        token = Mock()
        token.id = 'test-token-123'
        token.rate_limit_override = None

        request = Mock()
        request.api_token = token
        view = Mock()

        result = throttle.allow_request(request, view)

        assert result is True
        assert hasattr(request, 'rate_limit_info')
        assert request.rate_limit_info['limit'] == 1000
        assert request.rate_limit_info['burst_limit'] == 100

    @patch('smoothschedule.platform.api.throttling.cache')
    @patch('smoothschedule.platform.api.throttling.time')
    def test_allow_request_with_custom_rate_limit_override(self, mock_time, mock_cache):
        """Should use custom rate limit when token has override."""
        mock_time.time.return_value = 1000.0
        mock_cache.get.return_value = 10
        mock_cache.incr.return_value = 11

        throttle = GlobalBurstRateThrottle()
        token = Mock()
        token.id = 'test-token-123'
        token.rate_limit_override = 5000  # Custom limit

        request = Mock()
        request.api_token = token
        view = Mock()

        result = throttle.allow_request(request, view)

        assert result is True
        assert request.rate_limit_info['limit'] == 5000

    @patch('smoothschedule.platform.api.throttling.cache')
    @patch('smoothschedule.platform.api.throttling.time')
    def test_allow_request_denied_when_hourly_limit_exceeded(self, mock_time, mock_cache):
        """Should deny request when hourly limit is exceeded."""
        mock_time.time.return_value = 1000.0
        mock_cache.get.return_value = 1001  # Over hourly limit

        throttle = GlobalBurstRateThrottle()
        token = Mock()
        token.id = 'test-token-123'
        token.rate_limit_override = None

        request = Mock()
        request.api_token = token
        view = Mock()

        result = throttle.allow_request(request, view)

        assert result is False
        assert throttle.wait_time > 0

    @patch('smoothschedule.platform.api.throttling.cache')
    @patch('smoothschedule.platform.api.throttling.time')
    def test_allow_request_denied_when_minute_limit_exceeded(self, mock_time, mock_cache):
        """Should deny request when minute limit is exceeded."""
        mock_time.time.return_value = 1000.0
        # First call for hourly returns under limit, second call for minute returns over limit
        mock_cache.get.side_effect = [10, 101]  # Hourly OK, minute exceeded
        mock_cache.incr.return_value = 11

        throttle = GlobalBurstRateThrottle()
        token = Mock()
        token.id = 'test-token-123'
        token.rate_limit_override = None

        request = Mock()
        request.api_token = token
        view = Mock()

        result = throttle.allow_request(request, view)

        assert result is False

    @patch('smoothschedule.platform.api.throttling.cache')
    def test_check_rate_creates_key_when_not_exists(self, mock_cache):
        """Should create cache key when it doesn't exist."""
        mock_cache.get.return_value = 0
        mock_cache.incr.side_effect = ValueError("Key doesn't exist")
        mock_cache.set = Mock()

        throttle = GlobalBurstRateThrottle()
        throttle.now = 1000.0
        throttle.token = Mock(id='test-123')

        allowed, remaining, reset = throttle._check_rate('hourly', 1000, 3600)

        assert allowed is True
        mock_cache.set.assert_called_once()

    @patch('smoothschedule.platform.api.throttling.cache')
    def test_check_rate_calculates_remaining_correctly(self, mock_cache):
        """Should calculate remaining requests correctly."""
        mock_cache.get.return_value = 500
        mock_cache.incr.return_value = 501

        throttle = GlobalBurstRateThrottle()
        throttle.now = 1000.0
        throttle.token = Mock(id='test-123')

        allowed, remaining, reset = throttle._check_rate('hourly', 1000, 3600)

        assert allowed is True
        assert remaining == 499  # 1000 - 501

    def test_wait_returns_wait_time_when_set(self):
        """Should return wait_time attribute when set."""
        throttle = GlobalBurstRateThrottle()
        throttle.wait_time = 120

        assert throttle.wait() == 120

    def test_wait_returns_default_when_not_set(self):
        """Should return default 60 seconds when wait_time not set."""
        throttle = GlobalBurstRateThrottle()

        assert throttle.wait() == 60


class TestRateLimitHeadersMixin:
    """Tests for RateLimitHeadersMixin class."""

    def test_finalize_response_adds_headers_when_rate_limit_info_present(self):
        """Should add rate limit headers when rate_limit_info is present."""
        from rest_framework.views import APIView

        class TestView(RateLimitHeadersMixin, APIView):
            pass

        view = TestView()

        request = Mock()
        request.rate_limit_info = {
            'limit': 1000,
            'remaining': 500,
            'reset': 1234567890,
            'burst_limit': 100,
            'burst_remaining': 50,
        }
        request.accepted_renderer = Mock()
        request.accepted_renderer.format = 'json'
        request.accepted_media_type = 'application/json'

        # Create a real response-like dict that can store headers
        response = Mock()
        response.headers = {}
        response.__setitem__ = lambda self, k, v: self.headers.__setitem__(k, v)
        response.__getitem__ = lambda self, k: self.headers.__getitem__(k)

        with patch.object(APIView, 'finalize_response', return_value=response):
            result = view.finalize_response(request, response)

        # Verify headers were added
        assert result.headers.get('X-RateLimit-Limit') == 1000
        assert result.headers.get('X-RateLimit-Remaining') == 500
        assert result.headers.get('X-RateLimit-Reset') == 1234567890
        assert result.headers.get('X-RateLimit-Burst-Limit') == 100
        assert result.headers.get('X-RateLimit-Burst-Remaining') == 50

    def test_finalize_response_skips_headers_when_no_rate_limit_info(self):
        """Should not add headers when rate_limit_info is not present."""
        from rest_framework.views import APIView

        class TestView(RateLimitHeadersMixin, APIView):
            pass

        view = TestView()
        request = Mock(spec=['user', 'accepted_renderer', 'accepted_media_type'])
        request.accepted_renderer = Mock()
        request.accepted_renderer.format = 'json'
        request.accepted_media_type = 'application/json'

        response = Mock()
        response.headers = {}
        response.__setitem__ = lambda self, k, v: self.headers.__setitem__(k, v)

        with patch.object(APIView, 'finalize_response', return_value=response):
            result = view.finalize_response(request, response)

        # No rate limit headers should be set
        assert 'X-RateLimit-Limit' not in result.headers


class TestGetThrottleResponseData:
    """Tests for get_throttle_response_data function."""

    def test_returns_error_data_with_rate_limit_info(self):
        """Should return proper error data when rate_limit_info is present."""
        request = Mock()
        request.rate_limit_info = {
            'reset': int(time.time()) + 120,
        }

        result = get_throttle_response_data(request)

        assert result['error'] == 'rate_limit_exceeded'
        assert 'message' in result
        assert result['retry_after'] > 0
        assert result['retry_after'] <= 120

    def test_returns_default_retry_after_when_no_rate_limit_info(self):
        """Should return default retry_after when no rate_limit_info."""
        request = Mock(spec=['user'])  # No rate_limit_info

        result = get_throttle_response_data(request)

        assert result['error'] == 'rate_limit_exceeded'
        assert result['retry_after'] >= 1

    @patch('smoothschedule.platform.api.throttling.time')
    def test_retry_after_minimum_is_one(self, mock_time):
        """Should return minimum retry_after of 1 second."""
        mock_time.time.return_value = 1000

        request = Mock()
        request.rate_limit_info = {
            'reset': 1000,  # Reset time is now
        }

        result = get_throttle_response_data(request)

        assert result['retry_after'] >= 1
