"""Tests for the metadata-cache hard capacity cap (LRU eviction).

TTL-only eviction had no upper bound, so heavy in-window caching let
metadata_cache_entities reach ~1.8M rows / 7.6 GB. evict_over_capacity adds an
LRU row ceiling. We test the pure decision function directly and the SQL
behavior against a real temp DB (proves it drops the LEAST-recently-accessed
rows, not arbitrary ones).
"""

from __future__ import annotations

import sqlite3
import sys
import types

import pytest

# Minimal stubs so importing core.metadata.cache doesn't drag in spotipy/config.
if "spotipy" not in sys.modules:
    spotipy = types.ModuleType("spotipy")
    spotipy.Spotify = object
    oauth2 = types.ModuleType("spotipy.oauth2")
    oauth2.SpotifyOAuth = object
    oauth2.SpotifyClientCredentials = object
    spotipy.oauth2 = oauth2
    sys.modules["spotipy"] = spotipy
    sys.modules["spotipy.oauth2"] = oauth2
if "config.settings" not in sys.modules:
    config_pkg = types.ModuleType("config")
    settings_mod = types.ModuleType("config.settings")

    class _DummyCM:
        def get(self, key, default=None):
            return default

        def get_active_media_server(self):
            return "plex"

    settings_mod.config_manager = _DummyCM()
    config_pkg.settings = settings_mod
    sys.modules["config"] = config_pkg
    sys.modules["config.settings"] = settings_mod

from core.metadata.cache import (  # noqa: E402
    MetadataCache,
    entities_to_evict_for_capacity,
)


# ── pure decision function ─────────────────────────────────────────────────

def test_evict_count_over_cap():
    assert entities_to_evict_for_capacity(1000, 250) == 750


def test_evict_count_at_or_under_cap_is_zero():
    assert entities_to_evict_for_capacity(250, 250) == 0
    assert entities_to_evict_for_capacity(10, 250) == 0


def test_cap_zero_or_negative_means_no_eviction():
    assert entities_to_evict_for_capacity(10_000, 0) == 0
    assert entities_to_evict_for_capacity(10_000, -1) == 0


def test_never_negative():
    assert entities_to_evict_for_capacity(0, 100) == 0


# ── evict_over_capacity SQL behavior (real temp DB) ────────────────────────

class _NonClosingConn:
    def __init__(self, real):
        self._real = real

    def cursor(self):
        return self._real.cursor()

    def commit(self):
        return self._real.commit()

    def close(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, *a):
        pass


class _TempCache(MetadataCache):
    """MetadataCache whose _get_db returns a shim over a shared in-memory DB
    holding just the entities table — enough to exercise evict_over_capacity."""

    def __init__(self):
        self._conn = sqlite3.connect(":memory:")
        self._conn.row_factory = sqlite3.Row
        self._conn.execute("""
            CREATE TABLE metadata_cache_entities (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                source TEXT, entity_type TEXT, entity_id TEXT,
                name TEXT, raw_json TEXT,
                last_accessed_at TIMESTAMP,
                ttl_days INTEGER DEFAULT 30
            )
        """)
        self._conn.commit()

    def _get_db(self):
        outer = self

        class _DB:
            def _get_connection(self_inner):
                return _NonClosingConn(outer._conn)
        return _DB()

    # NOTE: we deliberately do NOT override _run_maintenance_write — the test
    # exercises the REAL method (retry + connection handling) so we're testing
    # production code, not a stub. _get_db is the only injected seam.


def _add_rows(cache, specs):
    """specs: list of (entity_id, last_accessed_at). Inserts rows."""
    cur = cache._conn.cursor()
    for eid, ts in specs:
        cur.execute(
            "INSERT INTO metadata_cache_entities (source, entity_type, entity_id, name, raw_json, last_accessed_at) "
            "VALUES ('spotify','artist',?,?, '{}', ?)",
            (eid, eid, ts),
        )
    cache._conn.commit()


def _ids(cache):
    return [r["entity_id"] for r in cache._conn.execute(
        "SELECT entity_id FROM metadata_cache_entities ORDER BY entity_id"
    ).fetchall()]


def test_evict_over_capacity_drops_least_recently_accessed():
    cache = _TempCache()
    # 5 rows, distinct access times. Cap at 3 -> evict the 2 oldest-accessed.
    _add_rows(cache, [
        ("a", "2026-05-01T00:00:00"),  # oldest -> evicted
        ("b", "2026-05-02T00:00:00"),  # oldest -> evicted
        ("c", "2026-05-03T00:00:00"),
        ("d", "2026-05-04T00:00:00"),
        ("e", "2026-05-05T00:00:00"),  # newest -> kept
    ])
    evicted = cache.evict_over_capacity(max_rows=3)
    assert evicted == 2
    assert _ids(cache) == ["c", "d", "e"]   # a, b gone (LRU)


def test_evict_over_capacity_noop_under_cap():
    cache = _TempCache()
    _add_rows(cache, [("a", "2026-05-01T00:00:00"), ("b", "2026-05-02T00:00:00")])
    assert cache.evict_over_capacity(max_rows=10) == 0
    assert _ids(cache) == ["a", "b"]


def test_evict_over_capacity_disabled_with_zero_cap():
    cache = _TempCache()
    _add_rows(cache, [(str(i), f"2026-05-0{i}T00:00:00") for i in range(1, 6)])
    assert cache.evict_over_capacity(max_rows=0) == 0
    assert len(_ids(cache)) == 5


def test_null_access_times_evicted_first():
    """Rows never accessed since insert (NULL last_accessed_at) are the
    coldest — they should go before any touched row."""
    cache = _TempCache()
    _add_rows(cache, [
        ("never1", None),
        ("never2", None),
        ("touched", "2026-05-01T00:00:00"),
    ])
    evicted = cache.evict_over_capacity(max_rows=1)
    assert evicted == 2
    assert _ids(cache) == ["touched"]
