"""Tests for the ExactHashCache (Tier 0 cache)."""

from __future__ import annotations

import asyncio
import json
from datetime import datetime  # noqa: F401 -- Pydantic forward ref resolution
from typing import TYPE_CHECKING

import pytest

from llmhosts.cache.exact import ExactHashCache
from llmhosts.cache.models import CacheEntry, CacheStats

if TYPE_CHECKING:
    from pathlib import Path

CacheEntry.model_rebuild()
CacheStats.model_rebuild()


# -------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------


@pytest.fixture()
async def cache(tmp_path: Path) -> ExactHashCache:
    """Create an initialised ExactHashCache backed by a temp SQLite DB."""
    c = ExactHashCache(db_path=tmp_path / "cache.db", max_size_mb=1, ttl_seconds=3600)
    await c.initialize()
    yield c  # type: ignore[misc]
    await c.close()


def _sample_messages(text: str = "Hello") -> list[dict]:
    return [{"role": "user", "content": text}]


def _sample_response() -> str:
    return json.dumps({"content": "Hi there!", "model": "test"})


# -------------------------------------------------------------------
# Put / Get round trip
# -------------------------------------------------------------------


@pytest.mark.unit
async def test_put_get_round_trip(cache: ExactHashCache) -> None:
    messages = _sample_messages()
    response = _sample_response()

    key = await cache.put("test-model", messages, response)
    assert isinstance(key, str)
    assert len(key) == 64  # SHA-256 hex

    entry = await cache.get("test-model", messages)
    assert entry is not None
    assert entry.response_json == response
    assert entry.model == "test-model"
    assert entry.hit_count == 1


@pytest.mark.unit
async def test_put_same_content_is_idempotent(cache: ExactHashCache) -> None:
    messages = _sample_messages()
    response = _sample_response()

    key1 = await cache.put("model-a", messages, response)
    key2 = await cache.put("model-a", messages, response)
    assert key1 == key2


# -------------------------------------------------------------------
# Cache miss
# -------------------------------------------------------------------


@pytest.mark.unit
async def test_get_miss_returns_none(cache: ExactHashCache) -> None:
    entry = await cache.get("model-a", _sample_messages("never stored"))
    assert entry is None


@pytest.mark.unit
async def test_different_model_is_miss(cache: ExactHashCache) -> None:
    """Same messages but different model should be a cache miss."""
    await cache.put("model-a", _sample_messages(), _sample_response())
    entry = await cache.get("model-b", _sample_messages())
    assert entry is None


# -------------------------------------------------------------------
# TTL expiration
# -------------------------------------------------------------------


@pytest.mark.unit
async def test_ttl_expiration(tmp_path: Path) -> None:
    """Entries with a very short TTL should expire and return None."""
    c = ExactHashCache(db_path=tmp_path / "exp.db", max_size_mb=1, ttl_seconds=0)
    await c.initialize()
    try:
        messages = _sample_messages()
        await c.put("model", messages, _sample_response())
        # TTL=0 means already expired by the time we fetch
        await asyncio.sleep(0.05)
        entry = await c.get("model", messages)
        assert entry is None
    finally:
        await c.close()


# -------------------------------------------------------------------
# Stats tracking
# -------------------------------------------------------------------


@pytest.mark.unit
async def test_stats_empty_cache(cache: ExactHashCache) -> None:
    stats = await cache.stats()
    assert stats.total_entries == 0
    assert stats.hit_count == 0
    assert stats.miss_count == 0
    assert stats.hit_rate == 0.0


@pytest.mark.unit
async def test_stats_after_operations(cache: ExactHashCache) -> None:
    messages = _sample_messages()
    await cache.put("model", messages, _sample_response())

    # One hit
    await cache.get("model", messages)
    # One miss
    await cache.get("model", _sample_messages("other"))

    stats = await cache.stats()
    assert stats.total_entries == 1
    assert stats.hit_count == 1
    assert stats.miss_count == 1
    assert stats.hit_rate == pytest.approx(0.5)


@pytest.mark.unit
async def test_stats_entries_by_model(cache: ExactHashCache) -> None:
    await cache.put("model-a", _sample_messages("a"), _sample_response())
    await cache.put("model-b", _sample_messages("b"), _sample_response())

    stats = await cache.stats()
    assert stats.total_entries == 2
    assert "model-a" in stats.entries_by_model
    assert "model-b" in stats.entries_by_model


# -------------------------------------------------------------------
# Clear / invalidate
# -------------------------------------------------------------------


@pytest.mark.unit
async def test_clear_all(cache: ExactHashCache) -> None:
    await cache.put("model", _sample_messages("a"), _sample_response())
    await cache.put("model", _sample_messages("b"), _sample_response())
    count = await cache.clear()
    assert count == 2
    stats = await cache.stats()
    assert stats.total_entries == 0


@pytest.mark.unit
async def test_clear_by_model(cache: ExactHashCache) -> None:
    await cache.put("model-a", _sample_messages("a"), _sample_response())
    await cache.put("model-b", _sample_messages("b"), _sample_response())
    count = await cache.clear(model="model-a")
    assert count == 1
    stats = await cache.stats()
    assert stats.total_entries == 1


@pytest.mark.unit
async def test_invalidate_specific_key(cache: ExactHashCache) -> None:
    key = await cache.put("model", _sample_messages(), _sample_response())
    removed = await cache.invalidate(key)
    assert removed is True
    entry = await cache.get("model", _sample_messages())
    assert entry is None


@pytest.mark.unit
async def test_invalidate_nonexistent_key(cache: ExactHashCache) -> None:
    removed = await cache.invalidate("nonexistent_key_hash")
    assert removed is False
