"""Tests for cache system."""
import pytest

from logos_adk.cache import (
    CacheConfig,
    CacheEntry,
    InMemoryCacheBackend,
)
from logos_adk.types import Response, ToolCall, Usage


def _make_response(content="hello", input_tokens=10, output_tokens=5):
    return Response(
        content=content,
        tool_calls=[],
        usage=Usage(input_tokens, output_tokens),
        raw={"provider": "test"},
    )


class TestCacheEntry:
    def test_from_response(self):
        r = _make_response("hi")
        entry = CacheEntry.from_response(r)
        assert entry.response_data["content"] == "hi"
        assert entry.response_data["usage"]["input_tokens"] == 10
        assert entry.hit_count == 0
        assert entry.vector is None

    def test_to_response(self):
        r = _make_response("test output")
        entry = CacheEntry.from_response(r)
        restored = entry.to_response()
        assert restored.content == "test output"
        assert restored.usage.input_tokens == 10
        assert restored.usage.output_tokens == 5
        assert restored.tool_calls == []
        assert restored.raw == {}  # raw is not cached

    def test_from_response_with_tool_calls(self):
        r = Response(
            content="",
            tool_calls=[ToolCall(id="1", name="search", arguments={"q": "x"})],
            usage=Usage(10, 5),
            raw={},
        )
        entry = CacheEntry.from_response(r)
        restored = entry.to_response()
        assert len(restored.tool_calls) == 1
        assert restored.tool_calls[0].name == "search"

    def test_serializes_to_json(self):
        import json
        r = _make_response()
        entry = CacheEntry.from_response(r)
        json.dumps(entry.response_data)


class TestCacheConfig:
    def test_defaults(self):
        cfg = CacheConfig()
        assert cfg.ttl == 3600
        assert cfg.semantic is False
        assert cfg.embed_fn is None
        assert cfg.similarity_threshold == 0.95


class TestInMemoryCacheBackend:
    @pytest.mark.asyncio
    async def test_put_and_get(self):
        backend = InMemoryCacheBackend()
        entry = CacheEntry.from_response(_make_response("cached"))
        await backend.put("key1", entry, ttl=None)

        result = await backend.get("key1")
        assert result is not None
        assert result.response_data["content"] == "cached"

    @pytest.mark.asyncio
    async def test_get_missing_returns_none(self):
        backend = InMemoryCacheBackend()
        assert await backend.get("missing") is None

    @pytest.mark.asyncio
    async def test_invalidate(self):
        backend = InMemoryCacheBackend()
        entry = CacheEntry.from_response(_make_response())
        await backend.put("key1", entry, ttl=None)
        await backend.invalidate("key1")
        assert await backend.get("key1") is None

    @pytest.mark.asyncio
    async def test_clear(self):
        backend = InMemoryCacheBackend()
        entry = CacheEntry.from_response(_make_response())
        await backend.put("k1", entry, ttl=None)
        await backend.put("k2", entry, ttl=None)
        await backend.clear()
        assert await backend.get("k1") is None
        assert await backend.get("k2") is None

    @pytest.mark.asyncio
    async def test_lru_eviction(self):
        backend = InMemoryCacheBackend(maxsize=2)
        e1 = CacheEntry.from_response(_make_response("one"))
        e2 = CacheEntry.from_response(_make_response("two"))
        e3 = CacheEntry.from_response(_make_response("three"))
        await backend.put("k1", e1, ttl=None)
        await backend.put("k2", e2, ttl=None)
        await backend.put("k3", e3, ttl=None)  # evicts k1
        assert await backend.get("k1") is None
        assert await backend.get("k2") is not None
        assert await backend.get("k3") is not None

    @pytest.mark.asyncio
    async def test_hit_count_increments(self):
        backend = InMemoryCacheBackend()
        entry = CacheEntry.from_response(_make_response())
        await backend.put("k", entry, ttl=None)
        r1 = await backend.get("k")
        assert r1.hit_count == 1
        r2 = await backend.get("k")
        assert r2.hit_count == 2

    @pytest.mark.asyncio
    async def test_search_similar_default_empty(self):
        backend = InMemoryCacheBackend()
        results = await backend.search_similar([0.1, 0.2], threshold=0.9)
        assert results == []

    @pytest.mark.asyncio
    async def test_ttl_expiry(self):
        import time
        backend = InMemoryCacheBackend()
        entry = CacheEntry.from_response(_make_response())
        await backend.put("k", entry, ttl=0)
        time.sleep(0.01)
        assert await backend.get("k") is None

    @pytest.mark.asyncio
    async def test_search_similar_finds_match(self):
        backend = InMemoryCacheBackend()
        entry = CacheEntry.from_response(_make_response("found"))
        entry.vector = [1.0, 0.0, 0.0]
        entry.scope = "scope1"
        await backend.put("k1", entry, ttl=None)

        results = await backend.search_similar(
            [1.0, 0.0, 0.0], threshold=0.99, scope="scope1"
        )
        assert len(results) == 1
        assert results[0].response_data["content"] == "found"

    @pytest.mark.asyncio
    async def test_search_similar_filters_scope(self):
        backend = InMemoryCacheBackend()
        entry = CacheEntry.from_response(_make_response("wrong scope"))
        entry.vector = [1.0, 0.0, 0.0]
        entry.scope = "scope1"
        await backend.put("k1", entry, ttl=None)

        results = await backend.search_similar(
            [1.0, 0.0, 0.0], threshold=0.99, scope="scope2"
        )
        assert len(results) == 0


# ---------------------------------------------------------------------------
# CacheKeyBuilder
# ---------------------------------------------------------------------------

from logos_adk.cache.key import CacheKeyBuilder


class TestCacheKeyBuilder:
    def test_exact_key_deterministic(self):
        builder = CacheKeyBuilder()
        key1 = builder.exact(model="claude", system_prompt="You are a bot",
                             messages=[{"role": "user", "content": "hi"}], tools=None)
        key2 = builder.exact(model="claude", system_prompt="You are a bot",
                             messages=[{"role": "user", "content": "hi"}], tools=None)
        assert key1 == key2

    def test_exact_key_differs_on_model(self):
        builder = CacheKeyBuilder()
        key1 = builder.exact(model="claude", system_prompt=None,
                             messages=[{"role": "user", "content": "hi"}], tools=None)
        key2 = builder.exact(model="gpt-4", system_prompt=None,
                             messages=[{"role": "user", "content": "hi"}], tools=None)
        assert key1 != key2

    def test_exact_key_differs_on_prompt(self):
        builder = CacheKeyBuilder()
        key1 = builder.exact(model="claude", system_prompt=None,
                             messages=[{"role": "user", "content": "hello"}], tools=None)
        key2 = builder.exact(model="claude", system_prompt=None,
                             messages=[{"role": "user", "content": "bye"}], tools=None)
        assert key1 != key2

    def test_exact_key_with_tools(self):
        builder = CacheKeyBuilder()
        tools = [{"name": "search", "description": "Search"}]
        key1 = builder.exact(model="claude", system_prompt=None,
                             messages=[{"role": "user", "content": "hi"}], tools=tools)
        key2 = builder.exact(model="claude", system_prompt=None,
                             messages=[{"role": "user", "content": "hi"}], tools=None)
        assert key1 != key2

    def test_scope_hash_deterministic(self):
        builder = CacheKeyBuilder()
        h1 = builder.scope_hash(model="claude", system_prompt="sys", tools=None)
        h2 = builder.scope_hash(model="claude", system_prompt="sys", tools=None)
        assert h1 == h2

    def test_scope_hash_differs(self):
        builder = CacheKeyBuilder()
        h1 = builder.scope_hash(model="claude", system_prompt="a", tools=None)
        h2 = builder.scope_hash(model="claude", system_prompt="b", tools=None)
        assert h1 != h2


# ---------------------------------------------------------------------------
# SQLiteCacheBackend
# ---------------------------------------------------------------------------

from logos_adk.cache.sqlite import SQLiteCacheBackend
from logos_adk.cache.postgresql import PostgreSQLCacheBackend


class TestSQLiteCacheBackend:
    @pytest.mark.asyncio
    async def test_put_and_get(self, tmp_path):
        backend = SQLiteCacheBackend(str(tmp_path / "cache.db"))
        entry = CacheEntry.from_response(_make_response("sqlite cached"))
        await backend.put("key1", entry, ttl=None)

        result = await backend.get("key1")
        assert result is not None
        assert result.response_data["content"] == "sqlite cached"

    @pytest.mark.asyncio
    async def test_get_missing(self, tmp_path):
        backend = SQLiteCacheBackend(str(tmp_path / "cache.db"))
        assert await backend.get("missing") is None

    @pytest.mark.asyncio
    async def test_invalidate(self, tmp_path):
        backend = SQLiteCacheBackend(str(tmp_path / "cache.db"))
        entry = CacheEntry.from_response(_make_response())
        await backend.put("k1", entry, ttl=None)
        await backend.invalidate("k1")
        assert await backend.get("k1") is None

    @pytest.mark.asyncio
    async def test_clear(self, tmp_path):
        backend = SQLiteCacheBackend(str(tmp_path / "cache.db"))
        entry = CacheEntry.from_response(_make_response())
        await backend.put("k1", entry, ttl=None)
        await backend.put("k2", entry, ttl=None)
        await backend.clear()
        assert await backend.get("k1") is None

    @pytest.mark.asyncio
    async def test_ttl_expiry(self, tmp_path):
        import time
        backend = SQLiteCacheBackend(str(tmp_path / "cache.db"))
        entry = CacheEntry.from_response(_make_response())
        await backend.put("k", entry, ttl=0)
        time.sleep(0.01)
        assert await backend.get("k") is None

    @pytest.mark.asyncio
    async def test_hit_count(self, tmp_path):
        backend = SQLiteCacheBackend(str(tmp_path / "cache.db"))
        entry = CacheEntry.from_response(_make_response())
        await backend.put("k", entry, ttl=None)
        r1 = await backend.get("k")
        assert r1.hit_count == 1
        r2 = await backend.get("k")
        assert r2.hit_count == 2

    @pytest.mark.asyncio
    async def test_persistence(self, tmp_path):
        path = str(tmp_path / "cache.db")
        backend1 = SQLiteCacheBackend(path)
        entry = CacheEntry.from_response(_make_response("persist"))
        await backend1.put("k", entry, ttl=None)

        backend2 = SQLiteCacheBackend(path)
        result = await backend2.get("k")
        assert result is not None
        assert result.response_data["content"] == "persist"


class _FakeCursor:
    def __init__(self, fetchone_result=None, fetchall_result=None):
        self.fetchone_result = fetchone_result
        self.fetchall_result = fetchall_result or []
        self.executed: list[tuple[str, tuple | list | None]] = []

    def execute(self, query, params=None):
        self.executed.append((query, params))

    def fetchone(self):
        return self.fetchone_result

    def fetchall(self):
        return self.fetchall_result

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False


class _FakeConnection:
    def __init__(self, cursor):
        self._cursor = cursor
        self.commits = 0

    def cursor(self):
        return self._cursor

    def commit(self):
        self.commits += 1

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False


class TestPostgreSQLCacheBackend:
    def test_constructor_uses_explicit_dsn(self):
        backend = PostgreSQLCacheBackend("postgresql://user:pass@localhost/cache")
        assert backend._dsn == "postgresql://user:pass@localhost/cache"

    def test_constructor_resolves_env_dsn(self, monkeypatch):
        monkeypatch.setenv("DATABASE_URL", "postgresql://env/cache")
        backend = PostgreSQLCacheBackend()
        assert backend._dsn == "postgresql://env/cache"

    @pytest.mark.asyncio
    async def test_get_updates_hit_count(self):
        cursor = _FakeCursor(
            fetchone_result=({"content": "cached", "tool_calls": [], "usage": {"input_tokens": 1, "output_tokens": 1}}, "2026-01-01T00:00:00+00:00", 0, None, None, None)
        )
        connection = _FakeConnection(cursor)
        backend = PostgreSQLCacheBackend.__new__(PostgreSQLCacheBackend)
        backend._dsn = "postgresql://test"
        backend._table_name = "cache"
        backend._connect = lambda: connection

        result = await PostgreSQLCacheBackend.get(backend, "k1")

        assert result is not None
        assert result.hit_count == 1
        assert any("UPDATE cache SET hit_count" in query for query, _ in cursor.executed)

    @pytest.mark.asyncio
    async def test_search_similar_filters_scope(self):
        cursor = _FakeCursor(
            fetchall_result=[
                ({"content": "cached", "tool_calls": [], "usage": {"input_tokens": 1, "output_tokens": 1}}, "2026-01-01T00:00:00+00:00", 0, [1.0, 0.0], "scope-1", None),
            ]
        )
        connection = _FakeConnection(cursor)
        backend = PostgreSQLCacheBackend.__new__(PostgreSQLCacheBackend)
        backend._dsn = "postgresql://test"
        backend._table_name = "cache"
        backend._connect = lambda: connection

        results = await PostgreSQLCacheBackend.search_similar(
            backend,
            [1.0, 0.0],
            threshold=0.9,
            scope="scope-1",
        )

        assert len(results) == 1
        assert results[0].response_data["content"] == "cached"
