"""
Tests for the CAAE-inspired response cache.

Validates:
- Exact match cache hit/miss
- TTL expiry
- CAAE cost-model eviction (expensive responses survive, cheap ones evicted)
- Access frequency bonus
"""

import time

import pytest

from proxy.cache import CacheEntry, CacheStats, ResponseCache
from proxy.config import ProxyConfig
from proxy.schemas import (
    ChatCompletionRequest,
    ChatCompletionResponse,
    ChatMessage,
    Choice,
    ChoiceMessage,
    Usage,
)


def _make_request(
    content: str = "hello", model: str = "gpt-4o"
) -> ChatCompletionRequest:
    return ChatCompletionRequest(
        model=model,
        messages=[ChatMessage(role="user", content=content)],
        temperature=1.0,
    )


def _make_response(
    content: str = "world", prompt_tokens: int = 10, completion_tokens: int = 5
) -> ChatCompletionResponse:
    return ChatCompletionResponse(
        id="test-id",
        model="gpt-4o",
        choices=[
            Choice(
                index=0,
                message=ChoiceMessage(role="assistant", content=content),
                finish_reason="stop",
            )
        ],
        usage=Usage(
            prompt_tokens=prompt_tokens,
            completion_tokens=completion_tokens,
            total_tokens=prompt_tokens + completion_tokens,
        ),
    )


@pytest.fixture
def config():
    return ProxyConfig(
        cache_ttl_seconds=3600,
        cache_max_entries=5,
        cost_model_weight_prompt_tokens=1.2,
        cost_model_weight_model_cost=2.5,
        cost_model_decay_rate=0.001,
    )


@pytest.fixture
def cache(config):
    return ResponseCache(config)


class TestCacheHitMiss:
    def test_cache_miss_returns_none(self, cache):
        req = _make_request("test")
        assert cache.get(req) is None

    def test_cache_hit_returns_response(self, cache):
        req = _make_request("hello")
        resp = _make_response("world")
        cache.put(
            req,
            resp,
            prompt_tokens=10,
            completion_tokens=5,
            model_input_cost_per_1k=0.01,
        )

        result = cache.get(req)
        assert result is not None
        assert result.choices[0].message.content == "world"

    def test_different_messages_are_different_keys(self, cache):
        req1 = _make_request("hello")
        req2 = _make_request("goodbye")
        resp = _make_response("response")

        cache.put(
            req1,
            resp,
            prompt_tokens=10,
            completion_tokens=5,
            model_input_cost_per_1k=0.01,
        )

        assert cache.get(req1) is not None
        assert cache.get(req2) is None

    def test_different_models_are_different_keys(self, cache):
        req1 = _make_request("hello", model="gpt-4o")
        req2 = _make_request("hello", model="gpt-4o-mini")
        resp = _make_response()

        cache.put(
            req1,
            resp,
            prompt_tokens=10,
            completion_tokens=5,
            model_input_cost_per_1k=0.01,
        )

        assert cache.get(req1) is not None
        assert cache.get(req2) is None

    def test_stats_track_hits_and_misses(self, cache):
        req = _make_request("hello")
        resp = _make_response()

        cache.get(req)  # miss
        cache.put(
            req,
            resp,
            prompt_tokens=10,
            completion_tokens=5,
            model_input_cost_per_1k=0.01,
        )
        cache.get(req)  # hit

        assert cache.stats.hits == 1
        assert cache.stats.misses == 1
        assert cache.stats.hit_rate == 0.5


class TestCacheTTL:
    def test_expired_entry_returns_none(self):
        config = ProxyConfig(cache_ttl_seconds=0, cache_max_entries=100)
        cache = ResponseCache(config)
        req = _make_request("hello")
        resp = _make_response()

        cache.put(
            req,
            resp,
            prompt_tokens=10,
            completion_tokens=5,
            model_input_cost_per_1k=0.01,
        )
        # TTL=0 means it's already expired
        time.sleep(0.01)
        assert cache.get(req) is None
        assert cache.stats.ttl_evictions == 1


class TestCAAECostModelEviction:
    """Tests that the CAAE cost-model eviction keeps expensive responses
    and evicts cheap ones when the cache is full."""

    def test_evicts_cheapest_entry(self, config):
        """When cache is full, the entry with the lowest retention score
        (cheapest model + fewest tokens + oldest access) is evicted."""
        config.cache_max_entries = 3
        cache = ResponseCache(config)

        # Entry 1: cheap model, few tokens
        req_cheap = _make_request("cheap", model="gpt-4o-mini")
        cache.put(
            req_cheap,
            _make_response(prompt_tokens=10),
            prompt_tokens=10,
            completion_tokens=5,
            model_input_cost_per_1k=0.00015,  # gpt-4o-mini is cheap
        )

        # Entry 2: expensive model, many tokens
        req_expensive = _make_request("expensive", model="gpt-4o")
        cache.put(
            req_expensive,
            _make_response(prompt_tokens=5000),
            prompt_tokens=5000,
            completion_tokens=500,
            model_input_cost_per_1k=0.0025,  # gpt-4o is expensive
        )

        # Entry 3: medium
        req_medium = _make_request("medium", model="gpt-4o")
        cache.put(
            req_medium,
            _make_response(prompt_tokens=500),
            prompt_tokens=500,
            completion_tokens=50,
            model_input_cost_per_1k=0.0025,
        )

        # Cache is now full (3/3). Adding one more should evict the cheapest.
        req_new = _make_request("new entry", model="gpt-4o")
        cache.put(
            req_new,
            _make_response(prompt_tokens=1000),
            prompt_tokens=1000,
            completion_tokens=100,
            model_input_cost_per_1k=0.0025,
        )

        # The cheap entry should be evicted
        assert (
            cache.get(req_cheap) is None
        ), "Cheap entry should have been evicted by CAAE cost model"
        assert cache.get(req_expensive) is not None, "Expensive entry should survive"
        assert cache.stats.cost_model_evictions >= 1

    def test_frequently_accessed_entries_survive(self, config):
        """Access frequency gives a bonus, keeping popular entries alive."""
        config.cache_max_entries = 2
        cache = ResponseCache(config)

        # Entry 1: cheap but popular (accessed many times)
        req_popular = _make_request("popular", model="gpt-4o-mini")
        cache.put(
            req_popular,
            _make_response(prompt_tokens=10),
            prompt_tokens=10,
            completion_tokens=5,
            model_input_cost_per_1k=0.00015,
        )
        # Simulate multiple accesses
        for _ in range(20):
            cache.get(req_popular)

        # Entry 2: expensive but unpopular
        req_unpopular = _make_request("unpopular", model="gpt-4o")
        cache.put(
            req_unpopular,
            _make_response(prompt_tokens=100),
            prompt_tokens=100,
            completion_tokens=10,
            model_input_cost_per_1k=0.0025,
        )

        # Add a third entry to trigger eviction
        req_new = _make_request("new", model="gpt-4o")
        cache.put(
            req_new,
            _make_response(prompt_tokens=50),
            prompt_tokens=50,
            completion_tokens=5,
            model_input_cost_per_1k=0.0025,
        )

        # The popular entry should survive due to frequency bonus
        assert (
            cache.get(req_popular) is not None
        ), "Popular entry should survive due to frequency bonus"


class TestRetentionScore:
    def test_higher_tokens_give_higher_score(self, config):
        now = time.time()
        entry_small = CacheEntry(
            key="small",
            response=_make_response(),
            prompt_tokens=10,
            completion_tokens=5,
            model_input_cost_per_1k=0.01,
            created_at=now,
            last_accessed=now,
        )
        entry_large = CacheEntry(
            key="large",
            response=_make_response(),
            prompt_tokens=10000,
            completion_tokens=500,
            model_input_cost_per_1k=0.01,
            created_at=now,
            last_accessed=now,
        )
        assert entry_large.retention_score(config, now) > entry_small.retention_score(
            config, now
        )

    def test_older_entries_have_lower_score(self, config):
        now = time.time()
        entry_fresh = CacheEntry(
            key="fresh",
            response=_make_response(),
            prompt_tokens=100,
            completion_tokens=10,
            model_input_cost_per_1k=0.01,
            created_at=now,
            last_accessed=now,
        )
        entry_stale = CacheEntry(
            key="stale",
            response=_make_response(),
            prompt_tokens=100,
            completion_tokens=10,
            model_input_cost_per_1k=0.01,
            created_at=now - 10000,
            last_accessed=now - 10000,
        )
        assert entry_fresh.retention_score(config, now) > entry_stale.retention_score(
            config, now
        )

    def test_expensive_model_gives_higher_score(self, config):
        now = time.time()
        entry_cheap = CacheEntry(
            key="cheap",
            response=_make_response(),
            prompt_tokens=100,
            completion_tokens=10,
            model_input_cost_per_1k=0.00015,  # gpt-4o-mini
            created_at=now,
            last_accessed=now,
        )
        entry_expensive = CacheEntry(
            key="expensive",
            response=_make_response(),
            prompt_tokens=100,
            completion_tokens=10,
            model_input_cost_per_1k=0.015,  # o1
            created_at=now,
            last_accessed=now,
        )
        assert entry_expensive.retention_score(
            config, now
        ) > entry_cheap.retention_score(config, now)
