"""
Test suite for Task 3.1: Per-Provider Semaphores

This test suite validates that per-provider semaphores work correctly:
- Different providers can run concurrently without blocking each other
- Same provider requests respect concurrency limits
- Unknown providers fall back to default semaphore
- Semaphore limits are configurable via environment variables
"""

import pytest
import asyncio
import time
from unittest.mock import Mock, patch, AsyncMock
from fastapi.testclient import TestClient

# Test imports
from api.routes.complete import (
    _get_provider_semaphore,
    _provider_semaphores,
    PROVIDER_CONCURRENCY_LIMITS,
)


class TestProviderSemaphores:
    """Test per-provider semaphore functionality."""

    def test_provider_semaphore_limits(self):
        """Verify each provider has correct semaphore limits."""
        expected_providers = [
            "openai",
            "anthropic",
            "google",
            "xai",
            "fireworks",
            "novita",
            "deepseek",
            "mistral",
            "openrouter",
        ]
        
        for provider in expected_providers:
            assert provider in PROVIDER_CONCURRENCY_LIMITS, f"Provider {provider} missing from limits"
            assert PROVIDER_CONCURRENCY_LIMITS[provider] > 0, f"Provider {provider} has invalid limit"
            assert provider in _provider_semaphores, f"Provider {provider} missing semaphore"

    def test_get_provider_semaphore_known_provider(self):
        """Verify _get_provider_semaphore returns correct semaphore for known providers."""
        for provider in PROVIDER_CONCURRENCY_LIMITS.keys():
            sem = _get_provider_semaphore(provider)
            assert sem is not None, f"No semaphore for {provider}"
            assert sem == _provider_semaphores[provider], f"Wrong semaphore for {provider}"

    def test_get_provider_semaphore_case_insensitive(self):
        """Verify provider name matching is case-insensitive."""
        sem_lower = _get_provider_semaphore("openai")
        sem_upper = _get_provider_semaphore("OPENAI")
        sem_mixed = _get_provider_semaphore("OpenAI")
        
        assert sem_lower == sem_upper == sem_mixed, "Provider matching should be case-insensitive"

    def test_get_provider_semaphore_whitespace(self):
        """Verify provider name handles whitespace correctly."""
        sem_clean = _get_provider_semaphore("openai")
        sem_spaces = _get_provider_semaphore("  openai  ")
        
        assert sem_clean == sem_spaces, "Should handle whitespace in provider names"

    def test_get_provider_semaphore_unknown_provider(self):
        """Verify unknown providers get default semaphore."""
        unknown_providers = ["unknown_provider", "custom_llm", "test_provider"]
        
        default_sem = _get_provider_semaphore(unknown_providers[0])
        for provider in unknown_providers[1:]:
            sem = _get_provider_semaphore(provider)
            assert sem == default_sem, "All unknown providers should use same default semaphore"

    def test_get_provider_semaphore_none_provider(self):
        """Verify None or empty provider gets default semaphore."""
        sem_none = _get_provider_semaphore(None)
        sem_empty = _get_provider_semaphore("")
        
        assert sem_none == sem_empty, "None and empty should both get default semaphore"

    @pytest.mark.asyncio
    async def test_semaphore_acquire_release(self):
        """Verify semaphore can be acquired and released."""
        provider = "openai"
        sem = _get_provider_semaphore(provider)
        
        # Acquire
        await sem.acquire()
        
        # Release
        sem.release()
        
        # Should be able to acquire again
        await sem.acquire()
        sem.release()

    @pytest.mark.asyncio
    async def test_different_providers_dont_block(self):
        """
        Verify requests to different providers can run concurrently.
        
        This is the key benefit of per-provider semaphores:
        slow OpenAI requests don't block fast Anthropic requests.
        """
        openai_sem = _get_provider_semaphore("openai")
        anthropic_sem = _get_provider_semaphore("anthropic")
        
        # Acquire all OpenAI slots (simulate OpenAI being slow)
        openai_limit = PROVIDER_CONCURRENCY_LIMITS["openai"]
        for _ in range(openai_limit):
            await openai_sem.acquire()
        
        # Anthropic should still be available (not blocked by OpenAI)
        start = time.time()
        await anthropic_sem.acquire()
        elapsed = time.time() - start
        
        # Should acquire immediately (no blocking)
        assert elapsed < 0.1, f"Anthropic was blocked by OpenAI (took {elapsed:.2f}s)"
        
        # Cleanup
        anthropic_sem.release()
        for _ in range(openai_limit):
            openai_sem.release()

    @pytest.mark.asyncio
    async def test_same_provider_respects_limit(self):
        """
        Verify same provider requests respect concurrency limit.
        
        If all semaphore slots are taken, the next request should block.
        """
        provider = "xai"  # Use small limit for faster test
        sem = _get_provider_semaphore(provider)
        limit = PROVIDER_CONCURRENCY_LIMITS[provider]
        
        # Acquire all slots
        for _ in range(limit):
            await sem.acquire()
        
        # Next acquire should block (we'll test with timeout)
        try:
            await asyncio.wait_for(sem.acquire(), timeout=0.1)
            # If we get here, semaphore didn't block (test failed)
            sem.release()
            pytest.fail(f"Semaphore for {provider} didn't enforce limit of {limit}")
        except asyncio.TimeoutError:
            # Expected: semaphore blocked because all slots taken
            pass
        
        # Cleanup
        for _ in range(limit):
            sem.release()

    @pytest.mark.asyncio
    async def test_concurrent_different_providers_performance(self):
        """
        Load test: Verify different providers can run truly in parallel.
        
        We'll simulate 50 OpenAI + 30 Anthropic concurrent "requests" (just acquires).
        They should all complete quickly because they don't block each other.
        """
        
        async def mock_request(provider: str, duration: float = 0.01):
            """Simulate a provider request."""
            sem = _get_provider_semaphore(provider)
            await sem.acquire()
            try:
                await asyncio.sleep(duration)  # Simulate work
            finally:
                sem.release()
        
        start = time.time()
        
        # Launch concurrent "requests" to different providers
        tasks = []
        tasks.extend([mock_request("openai") for _ in range(50)])
        tasks.extend([mock_request("anthropic") for _ in range(30)])
        tasks.extend([mock_request("google") for _ in range(40)])
        
        await asyncio.gather(*tasks)
        
        elapsed = time.time() - start
        
        # With per-provider semaphores, these should run in parallel
        # Expected: ~0.01s (duration of single request)
        # If they were blocking each other: ~1.2s (120 * 0.01)
        assert elapsed < 0.5, f"Requests took {elapsed:.2f}s (should be ~0.01s with parallel execution)"

    @pytest.mark.asyncio
    async def test_semaphore_fairness(self):
        """
        Verify semaphore is fair (FIFO ordering).
        
        If 3 requests arrive when semaphore is full, they should
        complete in arrival order once slots free up.
        """
        provider = "deepseek"
        sem = _get_provider_semaphore(provider)
        limit = PROVIDER_CONCURRENCY_LIMITS[provider]
        
        # Fill all slots
        for _ in range(limit):
            await sem.acquire()
        
        # Track completion order
        completed = []
        
        async def wait_and_record(request_id: int):
            await sem.acquire()
            completed.append(request_id)
            sem.release()
        
        # Start 3 waiting tasks
        tasks = [
            asyncio.create_task(wait_and_record(1)),
            asyncio.create_task(wait_and_record(2)),
            asyncio.create_task(wait_and_record(3)),
        ]
        
        # Wait a bit to ensure all tasks are waiting
        await asyncio.sleep(0.1)
        
        # Release slots one by one
        for _ in range(3):
            sem.release()
            await asyncio.sleep(0.05)  # Give time for task to acquire
        
        # Wait for all tasks to complete
        await asyncio.gather(*tasks)
        
        # Verify FIFO order
        assert completed == [1, 2, 3], f"Semaphore not fair: {completed}"
        
        # Cleanup remaining slots
        for _ in range(limit - 3):
            sem.release()


class TestProviderSemaphoreConfiguration:
    """Test configuration of provider semaphore limits."""

    def test_limits_are_reasonable(self):
        """Verify semaphore limits are reasonable (not too low, not too high)."""
        for provider, limit in PROVIDER_CONCURRENCY_LIMITS.items():
            assert limit >= 10, f"{provider} limit too low: {limit}"
            assert limit <= 100, f"{provider} limit too high: {limit}"

    def test_openai_has_highest_limit(self):
        """Verify OpenAI has highest limit (it handles concurrency best)."""
        openai_limit = PROVIDER_CONCURRENCY_LIMITS["openai"]
        for provider, limit in PROVIDER_CONCURRENCY_LIMITS.items():
            if provider != "openai" and provider != "openrouter":
                assert limit <= openai_limit, f"{provider} has higher limit than OpenAI"


class TestProviderSemaphoreIntegration:
    """Integration tests with actual API calls (mocked LLM)."""

    @pytest.mark.asyncio
    @pytest.mark.slow
    async def test_load_test_100_concurrent_requests(self):
        """
        Load test: 100 concurrent requests to different providers.
        
        This simulates real-world load with multiple providers.
        Verify:
        1. All requests complete successfully
        2. Requests to different providers don't block each other
        3. Response times are reasonable
        """
        
        async def mock_llm_request(provider: str, request_id: int):
            """Mock an LLM request with semaphore."""
            sem = _get_provider_semaphore(provider)
            start = time.time()
            
            await sem.acquire()
            try:
                # Simulate LLM call (50ms)
                await asyncio.sleep(0.05)
                elapsed = time.time() - start
                return {"request_id": request_id, "provider": provider, "elapsed": elapsed}
            finally:
                sem.release()
        
        # Mix of providers
        providers = [
            "openai", "openai", "openai", "openai",  # 40 OpenAI
            "anthropic", "anthropic", "anthropic",  # 30 Anthropic
            "google", "google",  # 20 Google
            "xai",  # 10 xAI
        ] * 10  # Total: 100 requests
        
        start = time.time()
        
        # Launch all requests concurrently
        tasks = [mock_llm_request(provider, i) for i, provider in enumerate(providers)]
        results = await asyncio.gather(*tasks)
        
        total_elapsed = time.time() - start
        
        # Verify all completed
        assert len(results) == 100, f"Expected 100 results, got {len(results)}"
        
        # With per-provider semaphores, this should complete much faster
        # than with a global semaphore
        # Expected: ~0.3s (with parallelism)
        # With global semaphore (20 limit): ~0.3s per batch * 5 batches = 1.5s
        assert total_elapsed < 1.0, f"Load test took {total_elapsed:.2f}s (too slow)"
        
        print(f"\n✅ Load test completed in {total_elapsed:.2f}s")
        print(f"   Average request time: {sum(r['elapsed'] for r in results) / len(results):.3f}s")


# Manual testing instructions
"""
MANUAL TESTING CHECKLIST FOR TASK 3.1

1. Test basic functionality:
   - Send request to OpenAI
   - Send request to Anthropic
   - Verify both complete successfully

2. Test concurrent requests to different providers:
   - Send 50 OpenAI requests in parallel
   - Send 30 Anthropic requests in parallel (simultaneously)
   - Verify both sets complete without blocking each other
   - Verify no rate limit errors

3. Test concurrent requests to same provider:
   - Send 60 OpenAI requests in parallel (exceeds limit of 50)
   - Verify first 50 start immediately
   - Verify next 10 wait for slots to free up
   - Verify all complete successfully

4. Test unknown provider:
   - Send request to custom/unknown provider
   - Verify it uses default semaphore
   - Verify no errors

5. Monitor throughput improvement:
   - Baseline: Send 100 requests with global semaphore (before Phase 3)
   - After: Send 100 requests with per-provider semaphores
   - Compare total time (should be 2-3x faster)

6. Check logs:
   - Verify no "semaphore" errors
   - Verify no "deadlock" or "timeout" warnings
   - Verify provider names logged correctly

7. Test configuration:
   - Set PROVIDER_CONCURRENCY_OPENAI=10 in environment
   - Restart server
   - Verify OpenAI limit is now 10 (test by sending 15 requests)

EXPECTED RESULTS:
- Different providers don't block each other ✓
- Same provider respects concurrency limit ✓
- Unknown providers work with default semaphore ✓
- Throughput increases by 2-3x under mixed load ✓
- No errors or warnings in logs ✓
- Configuration changes take effect ✓
"""
