"""Tests for orxaq.kernel.cache — TTLCache, ResultCache, CacheRegistry."""

from __future__ import annotations

from unittest.mock import patch

import pytest

from orxaq.kernel.cache import (
    _SENTINEL,
    CacheRegistry,
    CacheStats,
    ResultCache,
    TTLCache,
    get_registry,
)

# ---------------------------------------------------------------------------
# TTLCache — basic operations
# ---------------------------------------------------------------------------


class TestTTLCacheBasic:
    def test_put_and_get(self):
        c = TTLCache()
        c.put("a", 1)
        assert c.get("a") == 1

    def test_get_miss_returns_sentinel(self):
        c = TTLCache()
        assert c.get("missing") is _SENTINEL

    def test_overwrite_existing_key(self):
        c = TTLCache()
        c.put("a", 1)
        c.put("a", 2)
        assert c.get("a") == 2
        assert len(c) == 1

    def test_invalidate_existing_key(self):
        c = TTLCache()
        c.put("a", 1)
        assert c.invalidate("a") is True
        assert c.get("a") is _SENTINEL

    def test_invalidate_missing_key(self):
        c = TTLCache()
        assert c.invalidate("nope") is False

    def test_clear_resets_everything(self):
        c = TTLCache()
        c.put("a", 1)
        c.get("a")
        c.get("miss")
        c.clear()
        assert len(c) == 0
        assert c.hits == 0
        assert c.misses == 0
        assert c.evictions == 0

    def test_cache_none_value(self):
        """None is a valid cached value — should not be confused with miss."""
        c = TTLCache()
        c.put("n", None)
        assert c.get("n") is None
        assert c.get("n") is not _SENTINEL

    def test_len(self):
        c = TTLCache()
        c.put("a", 1)
        c.put("b", 2)
        assert len(c) == 2

    def test_maxsize_minimum_is_one(self):
        c = TTLCache(maxsize=0)
        c.put("a", 1)
        assert len(c) == 1


# ---------------------------------------------------------------------------
# TTLCache — LRU eviction
# ---------------------------------------------------------------------------


class TestTTLCacheLRU:
    def test_evicts_oldest_when_full(self):
        c = TTLCache(maxsize=2)
        c.put("a", 1)
        c.put("b", 2)
        c.put("c", 3)  # should evict "a"
        assert c.get("a") is _SENTINEL
        assert c.get("b") == 2
        assert c.get("c") == 3

    def test_access_refreshes_lru_order(self):
        c = TTLCache(maxsize=2)
        c.put("a", 1)
        c.put("b", 2)
        c.get("a")  # refresh "a"
        c.put("c", 3)  # should evict "b" (least recent)
        assert c.get("a") == 1
        assert c.get("b") is _SENTINEL
        assert c.get("c") == 3

    def test_overwrite_refreshes_lru_order(self):
        c = TTLCache(maxsize=2)
        c.put("a", 1)
        c.put("b", 2)
        c.put("a", 10)  # refresh "a"
        c.put("c", 3)  # should evict "b"
        assert c.get("a") == 10
        assert c.get("b") is _SENTINEL


# ---------------------------------------------------------------------------
# TTLCache — TTL expiry
# ---------------------------------------------------------------------------


class TestTTLCacheExpiry:
    @patch("orxaq.kernel.cache.time.monotonic")
    def test_expired_entry_returns_sentinel(self, mock_mono):
        mock_mono.return_value = 100.0
        c = TTLCache(ttl_seconds=10.0)
        c.put("a", 1)

        mock_mono.return_value = 111.0  # 11s later, past 10s TTL
        assert c.get("a") is _SENTINEL
        assert c.evictions == 1

    @patch("orxaq.kernel.cache.time.monotonic")
    def test_entry_valid_within_ttl(self, mock_mono):
        mock_mono.return_value = 100.0
        c = TTLCache(ttl_seconds=10.0)
        c.put("a", 1)

        mock_mono.return_value = 109.0  # 9s later, within TTL
        assert c.get("a") == 1


# ---------------------------------------------------------------------------
# TTLCache — statistics
# ---------------------------------------------------------------------------


class TestTTLCacheStats:
    def test_hits_and_misses(self):
        c = TTLCache()
        c.put("a", 1)
        c.get("a")  # hit
        c.get("a")  # hit
        c.get("x")  # miss
        assert c.hits == 2
        assert c.misses == 1

    def test_eviction_count(self):
        c = TTLCache(maxsize=1)
        c.put("a", 1)
        c.put("b", 2)  # evicts "a"
        c.put("c", 3)  # evicts "b"
        assert c.evictions == 2

    @patch("orxaq.kernel.cache.time.monotonic")
    def test_expiry_counts_as_eviction(self, mock_mono):
        mock_mono.return_value = 100.0
        c = TTLCache(ttl_seconds=5.0)
        c.put("a", 1)
        mock_mono.return_value = 200.0
        c.get("a")  # expired — eviction
        assert c.evictions == 1
        assert c.misses == 1


# ---------------------------------------------------------------------------
# ResultCache — function memoization
# ---------------------------------------------------------------------------


class TestResultCache:
    def test_caches_function_result(self):
        rc = ResultCache()
        call_count = 0

        @rc.cached
        def add(a, b):
            nonlocal call_count
            call_count += 1
            return a + b

        assert add(1, 2) == 3
        assert add(1, 2) == 3
        assert call_count == 1  # second call from cache

    def test_different_args_produce_different_entries(self):
        rc = ResultCache()

        @rc.cached
        def square(x):
            return x * x

        assert square(3) == 9
        assert square(4) == 16
        assert rc.cache.hits == 0  # all misses so far
        assert square(3) == 9
        assert rc.cache.hits == 1

    def test_kwargs_are_cached(self):
        rc = ResultCache()
        call_count = 0

        @rc.cached
        def greet(name, greeting="hello"):
            nonlocal call_count
            call_count += 1
            return f"{greeting} {name}"

        assert greet("bob", greeting="hi") == "hi bob"
        assert greet("bob", greeting="hi") == "hi bob"
        assert call_count == 1

    def test_preserves_function_metadata(self):
        rc = ResultCache()

        @rc.cached
        def my_func():
            """My docstring."""

        assert my_func.__name__ == "my_func"
        assert my_func.__doc__ == "My docstring."

    def test_cache_attribute_exposed(self):
        rc = ResultCache()

        @rc.cached
        def f():
            pass

        assert f.cache is rc.cache

    def test_caches_none_return(self):
        rc = ResultCache()
        call_count = 0

        @rc.cached
        def returns_none():
            nonlocal call_count
            call_count += 1
            return None

        assert returns_none() is None
        assert returns_none() is None
        assert call_count == 1


# ---------------------------------------------------------------------------
# CacheRegistry
# ---------------------------------------------------------------------------


class TestCacheRegistry:
    def test_register_and_get(self):
        reg = CacheRegistry()
        c = TTLCache()
        reg.register("test", c)
        assert reg.get("test") is c

    def test_get_missing_returns_none(self):
        reg = CacheRegistry()
        assert reg.get("nope") is None

    def test_unregister_existing(self):
        reg = CacheRegistry()
        reg.register("a", TTLCache())
        assert reg.unregister("a") is True
        assert reg.get("a") is None

    def test_unregister_missing(self):
        reg = CacheRegistry()
        assert reg.unregister("nope") is False

    def test_names_sorted(self):
        reg = CacheRegistry()
        reg.register("z", TTLCache())
        reg.register("a", TTLCache())
        reg.register("m", TTLCache())
        assert reg.names() == ["a", "m", "z"]

    def test_clear_all(self):
        reg = CacheRegistry()
        c1 = TTLCache()
        c1.put("x", 1)
        c2 = TTLCache()
        c2.put("y", 2)
        reg.register("c1", c1)
        reg.register("c2", c2)
        reg.clear_all()
        assert len(c1) == 0
        assert len(c2) == 0

    def test_summary_aggregates(self):
        reg = CacheRegistry()
        c1 = TTLCache(maxsize=2)
        c1.put("a", 1)
        c1.put("b", 2)
        c1.get("a")  # hit
        c1.get("miss")  # miss

        c2 = TTLCache(maxsize=1)
        c2.put("x", 10)
        c2.put("y", 20)  # evicts x

        reg.register("c1", c1)
        reg.register("c2", c2)

        stats = reg.summary()
        assert isinstance(stats, CacheStats)
        assert stats.total_caches == 2
        assert stats.total_entries == 3  # 2 in c1 + 1 in c2
        assert stats.total_hits == 1
        assert stats.total_misses == 1
        assert stats.total_evictions == 1


# ---------------------------------------------------------------------------
# CacheStats
# ---------------------------------------------------------------------------


class TestCacheStats:
    def test_hit_rate_normal(self):
        s = CacheStats(
            total_caches=1,
            total_entries=5,
            total_hits=3,
            total_misses=7,
            total_evictions=0,
        )
        assert s.hit_rate == pytest.approx(0.3)

    def test_hit_rate_no_lookups(self):
        s = CacheStats(
            total_caches=0,
            total_entries=0,
            total_hits=0,
            total_misses=0,
            total_evictions=0,
        )
        assert s.hit_rate == 0.0


# ---------------------------------------------------------------------------
# Module-level default registry
# ---------------------------------------------------------------------------


class TestGetRegistry:
    def test_returns_registry_instance(self):
        reg = get_registry()
        assert isinstance(reg, CacheRegistry)

    def test_returns_same_instance(self):
        assert get_registry() is get_registry()
