"""Unit tests for ``mgf.common.cache`` — C-5 federation primitive (v0.37.0)."""

from __future__ import annotations

import time

import pytest

from mgf.common.cache import Cache, CacheMiss, LRUCache, NullCache


class TestProtocolConformance:
    def test_lru_satisfies_cache(self) -> None:
        assert isinstance(LRUCache(maxsize=1), Cache)

    def test_null_satisfies_cache(self) -> None:
        assert isinstance(NullCache(), Cache)


class TestCacheMissSentinel:
    def test_is_falsy(self) -> None:
        assert not CacheMiss

    def test_repr(self) -> None:
        assert repr(CacheMiss) == "<CACHE_MISS>"

    def test_distinguishable_from_none(self) -> None:
        # The point of the sentinel: distinguish "no entry" from
        # "entry stored None".
        cache = LRUCache(maxsize=10)
        cache.set("k-none", None)
        assert cache.get("k-none") is None  # cached value
        assert cache.get("missing") is CacheMiss  # not cached


class TestLRUCacheBasic:
    def test_set_then_get(self) -> None:
        c = LRUCache(maxsize=10)
        c.set("k", "v")
        assert c.get("k") == "v"

    def test_get_miss_returns_sentinel(self) -> None:
        c = LRUCache(maxsize=10)
        assert c.get("nope") is CacheMiss

    def test_delete(self) -> None:
        c = LRUCache(maxsize=10)
        c.set("k", "v")
        c.delete("k")
        assert c.get("k") is CacheMiss

    def test_delete_missing_is_noop(self) -> None:
        c = LRUCache(maxsize=10)
        c.delete("never-existed")  # must not raise

    def test_clear(self) -> None:
        c = LRUCache(maxsize=10)
        for i in range(5):
            c.set(f"k{i}", i)
        c.clear()
        assert len(c) == 0


class TestLRUCacheTTL:
    def test_ttl_explicit_per_set(self) -> None:
        c = LRUCache(maxsize=10)
        c.set("k", "v", ttl_seconds=0.05)
        assert c.get("k") == "v"
        time.sleep(0.1)
        assert c.get("k") is CacheMiss

    def test_ttl_default(self) -> None:
        c = LRUCache(maxsize=10, default_ttl_seconds=0.05)
        c.set("k", "v")
        time.sleep(0.1)
        assert c.get("k") is CacheMiss

    def test_per_set_ttl_overrides_default(self) -> None:
        # Default is short; per-set is longer.
        c = LRUCache(maxsize=10, default_ttl_seconds=0.05)
        c.set("k", "v", ttl_seconds=10.0)
        time.sleep(0.1)
        assert c.get("k") == "v"  # not expired

    def test_no_ttl_no_expiry(self) -> None:
        c = LRUCache(maxsize=10)  # no default TTL
        c.set("k", "v")
        time.sleep(0.05)
        assert c.get("k") == "v"


class TestLRUEviction:
    def test_evicts_lru_on_overflow(self) -> None:
        c = LRUCache(maxsize=2)
        c.set("a", 1)
        c.set("b", 2)
        c.set("c", 3)  # evicts "a"
        assert c.get("a") is CacheMiss
        assert c.get("b") == 2
        assert c.get("c") == 3

    def test_get_bumps_to_mru(self) -> None:
        c = LRUCache(maxsize=2)
        c.set("a", 1)
        c.set("b", 2)
        c.get("a")  # bump "a" to most-recently-used
        c.set("c", 3)  # evicts "b" (now LRU)
        assert c.get("a") == 1
        assert c.get("b") is CacheMiss
        assert c.get("c") == 3

    def test_set_existing_bumps_to_mru(self) -> None:
        c = LRUCache(maxsize=2)
        c.set("a", 1)
        c.set("b", 2)
        c.set("a", 11)  # re-set "a" — bump
        c.set("c", 3)  # evicts "b"
        assert c.get("a") == 11
        assert c.get("b") is CacheMiss


class TestLRUConstruction:
    def test_invalid_maxsize(self) -> None:
        with pytest.raises(ValueError, match="maxsize must be > 0"):
            LRUCache(maxsize=0)

    def test_invalid_ttl(self) -> None:
        with pytest.raises(ValueError, match="default_ttl_seconds must be > 0"):
            LRUCache(maxsize=10, default_ttl_seconds=0)


class TestNullCache:
    def test_get_always_miss(self) -> None:
        c = NullCache()
        c.set("k", "v")
        assert c.get("k") is CacheMiss

    def test_delete_clear_no_op(self) -> None:
        c = NullCache()
        c.set("k", "v")
        c.delete("k")
        c.clear()
        # No raises; that's the contract.
