"""
Tests for Phase 93: Platform-Wide Caching Layer.
"""

import time

import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect cache storage to a temp directory and reset state."""
    cache_dir = tmp_path / "cache"
    cache_dir.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)

    import brynq.caching as mod
    monkeypatch.setattr(mod, "CACHE_DIR", cache_dir)
    monkeypatch.setattr(mod, "PERSISTENT_FILE", cache_dir / "persistent.json")
    mod._reset()


# ── 1. Set / Get ──────────────────────────────────────────────────────────


class TestSetGet:
    def test_set_and_get(self):
        from brynq.caching import cache_set, cache_get
        cache_set("key1", "value1")
        assert cache_get("key1") == "value1"

    def test_get_missing(self):
        from brynq.caching import cache_get
        assert cache_get("nonexistent") is None

    def test_overwrite(self):
        from brynq.caching import cache_set, cache_get
        cache_set("k", "v1")
        cache_set("k", "v2")
        assert cache_get("k") == "v2"

    def test_complex_value(self):
        from brynq.caching import cache_set, cache_get
        cache_set("data", {"nested": [1, 2, 3]})
        assert cache_get("data") == {"nested": [1, 2, 3]}

    def test_ttl_expiry(self):
        from brynq.caching import cache_set, cache_get
        cache_set("short", "val", ttl_seconds=0)
        time.sleep(0.01)
        assert cache_get("short") is None

    def test_long_ttl(self):
        from brynq.caching import cache_set, cache_get
        cache_set("long", "val", ttl_seconds=3600)
        assert cache_get("long") == "val"


# ── 2. Delete / Clear ────────────────────────────────────────────────────


class TestDeleteClear:
    def test_delete_existing(self):
        from brynq.caching import cache_set, cache_get, cache_delete
        cache_set("k", "v")
        result = cache_delete("k")
        assert result["removed"] is True
        assert cache_get("k") is None

    def test_delete_nonexistent(self):
        from brynq.caching import cache_delete
        result = cache_delete("nope")
        assert result["removed"] is False

    def test_clear(self):
        from brynq.caching import cache_set, cache_get, cache_clear
        cache_set("a", 1)
        cache_set("b", 2)
        result = cache_clear()
        assert result["cleared"] == 2
        assert cache_get("a") is None
        assert cache_get("b") is None

    def test_clear_empty(self):
        from brynq.caching import cache_clear
        result = cache_clear()
        assert result["cleared"] == 0


# ── 3. Tags / Invalidation ───────────────────────────────────────────────


class TestTags:
    def test_invalidate_by_tag(self):
        from brynq.caching import cache_set, cache_get, invalidate_by_tag
        cache_set("a", 1, tags=["group1"])
        cache_set("b", 2, tags=["group1"])
        cache_set("c", 3, tags=["group2"])
        result = invalidate_by_tag("group1")
        assert result["invalidated"] == 2
        assert cache_get("a") is None
        assert cache_get("b") is None
        assert cache_get("c") == 3

    def test_invalidate_no_match(self):
        from brynq.caching import cache_set, invalidate_by_tag
        cache_set("a", 1, tags=["other"])
        result = invalidate_by_tag("nope")
        assert result["invalidated"] == 0

    def test_multiple_tags(self):
        from brynq.caching import cache_set, cache_get, invalidate_by_tag
        cache_set("x", 1, tags=["t1", "t2"])
        invalidate_by_tag("t1")
        assert cache_get("x") is None

    def test_no_tags(self):
        from brynq.caching import cache_set, cache_get, invalidate_by_tag
        cache_set("x", 1)
        invalidate_by_tag("any")
        assert cache_get("x") == 1


# ── 4. Stats ──────────────────────────────────────────────────────────────


class TestStats:
    def test_initial_stats(self):
        from brynq.caching import get_cache_stats
        stats = get_cache_stats()
        assert stats["hits"] == 0
        assert stats["misses"] == 0
        assert stats["size"] == 0

    def test_hits_and_misses(self):
        from brynq.caching import cache_set, cache_get, get_cache_stats
        cache_set("k", "v")
        cache_get("k")       # hit
        cache_get("k")       # hit
        cache_get("miss")    # miss
        stats = get_cache_stats()
        assert stats["hits"] == 2
        assert stats["misses"] == 1

    def test_hit_rate(self):
        from brynq.caching import cache_set, cache_get, get_cache_stats
        cache_set("k", "v")
        cache_get("k")
        cache_get("miss")
        stats = get_cache_stats()
        assert stats["hit_rate"] == 0.5

    def test_eviction_count(self):
        from brynq.caching import cache_set, cache_delete, get_cache_stats
        cache_set("k", "v")
        cache_delete("k")
        stats = get_cache_stats()
        assert stats["evictions"] == 1

    def test_size_tracking(self):
        from brynq.caching import cache_set, cache_delete, get_cache_stats
        cache_set("a", 1)
        cache_set("b", 2)
        assert get_cache_stats()["size"] == 2
        cache_delete("a")
        assert get_cache_stats()["size"] == 1


# ── 5. Decorator ──────────────────────────────────────────────────────────


class TestDecorator:
    def test_caches_result(self):
        from brynq.caching import cache_decorator
        call_count = 0

        @cache_decorator(ttl_seconds=60)
        def expensive(x):
            nonlocal call_count
            call_count += 1
            return x * 2

        assert expensive(5) == 10
        assert expensive(5) == 10
        assert call_count == 1

    def test_different_args(self):
        from brynq.caching import cache_decorator
        call_count = 0

        @cache_decorator(ttl_seconds=60)
        def fn(x):
            nonlocal call_count
            call_count += 1
            return x

        fn(1)
        fn(2)
        assert call_count == 2

    def test_decorator_ttl(self):
        from brynq.caching import cache_decorator
        call_count = 0

        @cache_decorator(ttl_seconds=0)
        def fn():
            nonlocal call_count
            call_count += 1
            return "result"

        fn()
        time.sleep(0.01)
        fn()
        assert call_count == 2
