"""
Tests for retry logic with exponential backoff.
"""

import pytest
import asyncio
from authevo.retry import retry_with_backoff, with_timeout
from authevo.exceptions import NetworkError, RateLimitError


class TestRetryWithBackoff:
    """Test retry decorator functionality."""
    
    @pytest.mark.asyncio
    async def test_succeeds_first_try(self):
        """Test function that succeeds on first attempt."""
        call_count = 0
        
        @retry_with_backoff(max_retries=3)
        async def succeed_immediately():
            nonlocal call_count
            call_count += 1
            return "success"
        
        result = await succeed_immediately()
        assert result == "success"
        assert call_count == 1
    
    @pytest.mark.asyncio
    async def test_retries_on_network_error(self):
        """Test retries happen on NetworkError."""
        call_count = 0
        
        @retry_with_backoff(max_retries=3, base_delay=0.1)
        async def fail_twice():
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise NetworkError("Temporary failure")
            return "success"
        
        result = await fail_twice()
        assert result == "success"
        assert call_count == 3
    
    @pytest.mark.asyncio
    async def test_raises_after_max_retries(self):
        """Test exception is raised after max retries."""
        call_count = 0
        
        @retry_with_backoff(max_retries=3, base_delay=0.1)
        async def always_fail():
            nonlocal call_count
            call_count += 1
            raise NetworkError("Permanent failure")
        
        with pytest.raises(NetworkError):
            await always_fail()
        
        assert call_count == 3
    
    @pytest.mark.asyncio
    async def test_respects_rate_limit_retry_after(self):
        """Test RateLimitError uses retry_after value."""
        call_count = 0
        start_time = asyncio.get_event_loop().time()
        
        @retry_with_backoff(max_retries=2, base_delay=0.1)
        async def rate_limited():
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise RateLimitError(retry_after=1)  # 1 second wait
            return "success"
        
        result = await rate_limited()
        elapsed = asyncio.get_event_loop().time() - start_time
        
        assert result == "success"
        assert call_count == 2
        # Should have waited ~1 second (with some tolerance)
        assert 0.9 < elapsed < 1.5
    
    @pytest.mark.asyncio
    async def test_no_retry_on_unexpected_exception(self):
        """Test unexpected exceptions are not retried."""
        call_count = 0
        
        @retry_with_backoff(max_retries=3)
        async def unexpected_error():
            nonlocal call_count
            call_count += 1
            raise ValueError("Unexpected error")
        
        with pytest.raises(ValueError):
            await unexpected_error()
        
        # Should only be called once (no retries)
        assert call_count == 1
    
    @pytest.mark.asyncio
    async def test_exponential_backoff_timing(self):
        """Test backoff timing is exponential."""
        call_count = 0
        timestamps = []
        
        @retry_with_backoff(max_retries=4, base_delay=0.1, backoff_factor=2.0)
        async def track_timing():
            nonlocal call_count
            call_count += 1
            timestamps.append(asyncio.get_event_loop().time())
            if call_count < 4:
                raise NetworkError("Retry needed")
            return "success"
        
        await track_timing()
        
        # Check delays between attempts
        # Attempt 1->2: ~0.1s, Attempt 2->3: ~0.2s, Attempt 3->4: ~0.4s
        delays = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)]
        
        assert len(delays) == 3
        assert 0.05 < delays[0] < 0.15  # ~0.1s
        assert 0.15 < delays[1] < 0.25  # ~0.2s
        assert 0.35 < delays[2] < 0.45  # ~0.4s


class TestWithTimeout:
    """Test timeout decorator functionality."""
    
    @pytest.mark.asyncio
    async def test_completes_before_timeout(self):
        """Test function that completes before timeout."""
        @with_timeout(1.0)
        async def quick_operation():
            await asyncio.sleep(0.1)
            return "done"
        
        result = await quick_operation()
        assert result == "done"
    
    @pytest.mark.asyncio
    async def test_raises_on_timeout(self):
        """Test NetworkError raised when timeout exceeded."""
        @with_timeout(0.1)
        async def slow_operation():
            await asyncio.sleep(1.0)
            return "done"
        
        with pytest.raises(NetworkError) as exc_info:
            await slow_operation()
        
        assert "timed out" in str(exc_info.value).lower()
