"""
Test suite for Task 3.2: Routing Cache

This test suite validates that the routing cache works correctly:
- Cache correctly stores and retrieves routing decisions
- Cache respects TTL (time-to-live)
- Cache invalidation works
- Cache improves performance
"""

import pytest
import time
from api.services.routing_cache_service import RoutingCacheService, CachedRoutingResult


class TestRoutingCacheBasics:
    """Test basic routing cache functionality."""

    def test_cache_init(self):
        """Verify cache initializes with correct parameters."""
        cache = RoutingCacheService(ttl_seconds=30, max_size=100)
        
        assert cache.ttl_seconds == 30
        assert cache.max_size == 100
        assert len(cache._cache) == 0

    def test_cache_key_generation(self):
        """Verify cache keys are generated correctly."""
        cache = RoutingCacheService()
        
        # Same parameters should generate same key
        key1 = cache._make_cache_key(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test"},
            exclude_model_ids=None,
        )
        key2 = cache._make_cache_key(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test"},
            exclude_model_ids=None,
        )
        
        assert key1 == key2, "Same parameters should generate same key"
        
        # Different parameters should generate different keys
        key3 = cache._make_cache_key(
            subscription_id=124,  # Different subscription
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test"},
            exclude_model_ids=None,
        )
        
        assert key1 != key3, "Different subscription should generate different key"

    def test_cache_set_and_get(self):
        """Verify basic set/get operations work."""
        cache = RoutingCacheService(ttl_seconds=60)
        
        result = CachedRoutingResult(
            selected_provider="openai",
            selected_model="gpt-4",
            selected_model_id="openai/gpt-4",
            canonical_model_id="gpt-4",
            model_hle=0.95,
            model_price=30.0,
            input_price_per_million=30.0,
            output_price_per_million=60.0,
            effective_hle=0.95,
            requested_hle=0.9,
            hle_source="model",
            hle_clamped=False,
            timestamp=time.time(),
        )
        
        # Set
        cache.set(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test"},
            exclude_model_ids=None,
            result=result,
        )
        
        # Get
        cached = cache.get(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test"},
            exclude_model_ids=None,
        )
        
        assert cached is not None, "Should retrieve cached result"
        assert cached.selected_provider == "openai"
        assert cached.selected_model == "gpt-4"

    def test_cache_miss(self):
        """Verify cache miss returns None."""
        cache = RoutingCacheService()
        
        cached = cache.get(
            subscription_id=999,
            provider="anthropic",
            desired_hle=0.8,
            tags={},
            exclude_model_ids=None,
        )
        
        assert cached is None, "Cache miss should return None"

    def test_cache_invalidates_when_model_hle_exceeds_ceiling(self):
        """
        Prevent “HLE inflation” bugs from persisting via cache.

        Invariant: model_hle must be <= effective_hle when effective_hle is a ceiling.
        """
        cache = RoutingCacheService(ttl_seconds=60)

        bad = CachedRoutingResult(
            selected_provider="novita",
            selected_model="glm-4.7-flash",
            selected_model_id="novita/glm-4.7-flash",
            canonical_model_id="glm-4.7",
            model_hle=29.0,
            model_price=0.2,
            input_price_per_million=0.07,
            output_price_per_million=0.4,
            effective_hle=25.1,
            requested_hle=29.0,
            hle_source="request",
            hle_clamped=False,
            timestamp=time.time(),
        )

        cache.set(
            subscription_id=123,
            provider=None,
            desired_hle=29.0,
            tags={},
            exclude_model_ids=None,
            result=bad,
        )

        cached = cache.get(
            subscription_id=123,
            provider=None,
            desired_hle=29.0,
            tags={},
            exclude_model_ids=None,
        )

        assert cached is None, "Cache should invalidate entries that violate HLE ceiling invariant"

    def test_cache_ttl_expiration(self):
        """Verify cache entries expire after TTL."""
        cache = RoutingCacheService(ttl_seconds=1)  # 1 second TTL
        
        result = CachedRoutingResult(
            selected_provider="openai",
            selected_model="gpt-4",
            selected_model_id="openai/gpt-4",
            canonical_model_id="gpt-4",
            model_hle=0.95,
            model_price=30.0,
            input_price_per_million=30.0,
            output_price_per_million=60.0,
            effective_hle=0.95,
            requested_hle=0.9,
            hle_source="model",
            hle_clamped=False,
            timestamp=time.time(),
        )
        
        # Set
        cache.set(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={},
            exclude_model_ids=None,
            result=result,
        )
        
        # Should be available immediately
        cached = cache.get(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={},
            exclude_model_ids=None,
        )
        assert cached is not None, "Should be cached immediately"
        
        # Wait for expiration
        time.sleep(1.5)
        
        # Should be expired now
        cached = cache.get(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={},
            exclude_model_ids=None,
        )
        assert cached is None, "Cache should expire after TTL"

    def test_cache_eviction_on_max_size(self):
        """Verify cache evicts old entries when max size reached."""
        cache = RoutingCacheService(ttl_seconds=60, max_size=10)
        
        # Fill cache beyond max size
        for i in range(15):
            result = CachedRoutingResult(
                selected_provider="openai",
                selected_model=f"model-{i}",
                selected_model_id=f"openai/model-{i}",
                canonical_model_id=f"model-{i}",
                model_hle=0.9,
                model_price=30.0,
                input_price_per_million=30.0,
                output_price_per_million=60.0,
                effective_hle=0.9,
                requested_hle=0.9,
                hle_source="model",
                hle_clamped=False,
                timestamp=time.time(),
            )
            
            cache.set(
                subscription_id=i,
                provider="openai",
                desired_hle=0.9,
                tags={},
                exclude_model_ids=None,
                result=result,
            )
            
            time.sleep(0.01)  # Small delay to ensure timestamp ordering
        
        # Cache should not exceed max size
        assert len(cache._cache) <= cache.max_size, f"Cache size {len(cache._cache)} exceeds max {cache.max_size}"

    def test_cache_invalidate_subscription(self):
        """Verify subscription invalidation removes all related entries."""
        cache = RoutingCacheService(ttl_seconds=60)
        
        # Add entries for subscription 123
        for provider in ["openai", "anthropic", "google"]:
            result = CachedRoutingResult(
                selected_provider=provider,
                selected_model="test",
                selected_model_id=f"{provider}/test",
                canonical_model_id="test",
                model_hle=0.9,
                model_price=30.0,
                input_price_per_million=30.0,
                output_price_per_million=60.0,
                effective_hle=0.9,
                requested_hle=0.9,
                hle_source="model",
                hle_clamped=False,
                timestamp=time.time(),
            )
            
            cache.set(
                subscription_id=123,
                provider=provider,
                desired_hle=0.9,
                tags={},
                exclude_model_ids=None,
                result=result,
            )
        
        # Add entry for different subscription
        result = CachedRoutingResult(
            selected_provider="openai",
            selected_model="test",
            selected_model_id="openai/test",
            canonical_model_id="test",
            model_hle=0.9,
            model_price=30.0,
            input_price_per_million=30.0,
            output_price_per_million=60.0,
            effective_hle=0.9,
            requested_hle=0.9,
            hle_source="model",
            hle_clamped=False,
            timestamp=time.time(),
        )
        cache.set(
            subscription_id=456,
            provider="openai",
            desired_hle=0.9,
            tags={},
            exclude_model_ids=None,
            result=result,
        )
        
        # Should have 4 entries total
        assert len(cache._cache) == 4
        
        # Invalidate subscription 123
        cache.invalidate_subscription(123)
        
        # Should only have 1 entry left (subscription 456)
        assert len(cache._cache) == 1
        
        # Verify subscription 123 entries are gone
        for provider in ["openai", "anthropic", "google"]:
            cached = cache.get(
                subscription_id=123,
                provider=provider,
                desired_hle=0.9,
                tags={},
                exclude_model_ids=None,
            )
            assert cached is None, f"Subscription 123 {provider} should be invalidated"
        
        # Verify subscription 456 entry still exists
        cached = cache.get(
            subscription_id=456,
            provider="openai",
            desired_hle=0.9,
            tags={},
            exclude_model_ids=None,
        )
        assert cached is not None, "Subscription 456 should still be cached"

    def test_cache_stats(self):
        """Verify cache statistics tracking."""
        cache = RoutingCacheService(ttl_seconds=60)
        
        result = CachedRoutingResult(
            selected_provider="openai",
            selected_model="gpt-4",
            selected_model_id="openai/gpt-4",
            canonical_model_id="gpt-4",
            model_hle=0.95,
            model_price=30.0,
            input_price_per_million=30.0,
            output_price_per_million=60.0,
            effective_hle=0.95,
            requested_hle=0.9,
            hle_source="model",
            hle_clamped=False,
            timestamp=time.time(),
        )
        
        # Set
        cache.set(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={},
            exclude_model_ids=None,
            result=result,
        )
        
        # Cache hit
        cache.get(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={},
            exclude_model_ids=None,
        )
        
        # Cache miss
        cache.get(
            subscription_id=999,
            provider="anthropic",
            desired_hle=0.8,
            tags={},
            exclude_model_ids=None,
        )
        
        stats = cache.get_stats()
        
        assert stats["hits"] == 1, "Should have 1 hit"
        assert stats["misses"] == 1, "Should have 1 miss"
        assert stats["cache_size"] == 1, "Should have 1 entry"
        assert "hit_rate" in stats


class TestRoutingCacheTagHandling:
    """Test tag-based cache key generation."""

    def test_relevant_tags_only(self):
        """Verify only relevant tags affect cache key."""
        cache = RoutingCacheService()
        
        # Keys with same routing-relevant tags should match
        key1 = cache._make_cache_key(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test", "irrelevant": "value1"},
            exclude_model_ids=None,
        )
        key2 = cache._make_cache_key(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test", "irrelevant": "value2"},  # Different irrelevant tag
            exclude_model_ids=None,
        )
        
        assert key1 == key2, "Irrelevant tags shouldn't affect cache key"

    def test_routing_tags_affect_key(self):
        """Verify routing-relevant tags affect cache key."""
        cache = RoutingCacheService()
        
        # Keys with different project tags should differ
        key1 = cache._make_cache_key(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test1"},
            exclude_model_ids=None,
        )
        key2 = cache._make_cache_key(
            subscription_id=123,
            provider="openai",
            desired_hle=0.9,
            tags={"project": "test2"},
            exclude_model_ids=None,
        )
        
        assert key1 != key2, "Different project tags should generate different keys"


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

1. Test cache hit:
   - Send same request twice with identical parameters
   - Verify second request is faster (check logs for "Routing cache HIT")
   - Verify routing decision is same

2. Test cache miss:
   - Send request with new parameters
   - Verify logs show "Routing cache MISS"
   - Verify routing computation happens

3. Test cache expiration:
   - Send request, wait 61 seconds, send again
   - Verify cache expired (logs show "MISS" on second request)

4. Test different subscriptions:
   - Send request from subscription A
   - Send request from subscription B (same other params)
   - Verify both compute routing (no cache hit)

5. Test cache statistics:
   - Make several requests
   - Check cache stats in logs
   - Verify hit rate increases over time

6. Test performance improvement:
   - Measure routing time on cache miss (first request)
   - Measure routing time on cache hit (repeat request)
   - Verify 20-30ms improvement on cache hit

7. Test configuration:
   - Set ENABLE_ROUTING_CACHE=false
   - Restart server
   - Verify cache is disabled (no HIT/MISS logs)
   - Set back to true, verify cache works

EXPECTED RESULTS:
- Cache hit reduces routing time by 20-30ms ✓
- Cache respects 60s TTL ✓
- Different subscriptions have separate cache entries ✓
- Cache stats show hit rate increasing ✓
- Configuration toggle works ✓
"""
