"""Unit tests for the cache subsystem."""

import time
import pytest

from playwright_healer.cache import (
    CacheEntry,
    FileSelectorCache,
    MemorySelectorCache,
    build_cache,
)
from playwright_healer.config import CacheBackend, CacheConfig


def _cfg(**kwargs) -> CacheConfig:
    return CacheConfig(
        backend=kwargs.get("backend", CacheBackend.MEMORY),
        max_size=kwargs.get("max_size", 100),
        ttl_hours=kwargs.get("ttl_hours", 1.0),
        min_ttl_hours=0.1,
        max_ttl_hours=24.0,
        file_path=kwargs.get("file_path", ".test_cache.json"),
    )


def _entry(selector="#x", healed="#y", confidence=0.9) -> CacheEntry:
    return CacheEntry(
        original_selector=selector,
        healed_selector=healed,
        description="test element",
        url_pattern="https://example.com",
        stage="HEURISTIC",
        confidence=confidence,
        expires_at=time.time() + 3600,
    )


class TestMemoryCache:
    def setup_method(self):
        self.cache = MemorySelectorCache(_cfg())

    def test_put_and_get(self):
        e = _entry()
        key = self.cache._cache_key("#x", "test element")
        self.cache.put(key, e)
        result = self.cache.get(key)
        assert result is not None
        assert result.healed_selector == "#y"

    def test_miss_returns_none(self):
        assert self.cache.get("nonexistent") is None

    def test_expired_entry_removed(self):
        e = _entry()
        e.expires_at = time.time() - 1  # already expired
        key = "expired"
        self.cache.put(key, e)
        assert self.cache.get(key) is None

    def test_invalidate(self):
        e = _entry()
        key = "k"
        self.cache.put(key, e)
        self.cache.invalidate(key)
        assert self.cache.get(key) is None

    def test_clear(self):
        for i in range(5):
            self.cache.put(f"key{i}", _entry(f"#{i}", f"#h{i}"))
        self.cache.clear()
        assert self.cache.size() == 0

    def test_record_hit_increments(self):
        e = _entry()
        key = "hit_test"
        self.cache.put(key, e)
        self.cache.record_hit(key)
        assert self.cache.get(key).hit_count == 1

    def test_record_miss_increments(self):
        e = _entry()
        key = "miss_test"
        self.cache.put(key, e)
        self.cache.record_miss(key)
        assert self.cache.get(key).miss_count == 1

    def test_max_size_eviction(self):
        cache = MemorySelectorCache(_cfg(max_size=3))
        for i in range(5):
            cache.put(f"key{i}", _entry(f"#{i}", f"#h{i}"))
        assert cache.size() <= 3

    def test_cache_key_includes_description(self):
        key1 = self.cache._cache_key("#btn", "Submit button")
        key2 = self.cache._cache_key("#btn", "Cancel button")
        assert key1 != key2


class TestAdaptiveTTL:
    def test_stable_selector_gets_long_ttl(self):
        cfg = _cfg()
        cache = MemorySelectorCache(cfg)
        entry = _entry(confidence=1.0)
        entry.hit_count = 100
        entry.miss_count = 0
        ttl = cache._adaptive_ttl(entry)
        assert ttl >= cfg.ttl_hours * 3600

    def test_unstable_selector_gets_short_ttl(self):
        cfg = _cfg()
        cache = MemorySelectorCache(cfg)

        stable_entry = _entry(confidence=1.0)
        stable_entry.hit_count = 100
        stable_entry.miss_count = 0

        unstable_entry = _entry(confidence=0.5)
        unstable_entry.hit_count = 2
        unstable_entry.miss_count = 8

        stable_ttl = cache._adaptive_ttl(stable_entry)
        unstable_ttl = cache._adaptive_ttl(unstable_entry)

        # Unstable selectors should always get shorter TTL than stable ones
        assert unstable_ttl < stable_ttl, (
            f"Expected unstable_ttl ({unstable_ttl:.0f}s) < stable_ttl ({stable_ttl:.0f}s)"
        )
        # Unstable TTL should be below the max
        assert unstable_ttl < cfg.max_ttl_hours * 3600


class TestBuildCache:
    def test_builds_memory_cache(self):
        cache = build_cache(_cfg(backend=CacheBackend.MEMORY))
        assert isinstance(cache, MemorySelectorCache)

    def test_builds_file_cache(self, tmp_path):
        cfg = _cfg(backend=CacheBackend.FILE, file_path=str(tmp_path / "cache.json"))
        cache = build_cache(cfg)
        assert isinstance(cache, FileSelectorCache)


class TestFileCachePersistence:
    def test_survives_restart(self, tmp_path):
        path = str(tmp_path / "persist_test.json")
        cfg = _cfg(backend=CacheBackend.FILE, file_path=path)

        # Write
        c1 = FileSelectorCache(cfg)
        key = c1._cache_key("#x", "test")
        c1.put(key, _entry())

        # Re-load
        c2 = FileSelectorCache(cfg)
        result = c2.get(key)
        assert result is not None
        assert result.healed_selector == "#y"
