"""Tests for the per-sender model LRU registry in engines/kraken.py."""
from unittest.mock import MagicMock, call, patch

import pytest


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_registry(max_size=5):
    from engines.kraken import _SenderModelRegistry
    return _SenderModelRegistry(max_size=max_size)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

def test_cache_hit_returns_same_object():
    """Second get_model call with the same path must return the cached object."""
    registry = _make_registry()
    mock_model = MagicMock(name="model_a")
    with patch("engines.kraken._load_sender_model", return_value=mock_model) as loader:
        m1 = registry.get_model("/app/models/sender_a.mlmodel")
        m2 = registry.get_model("/app/models/sender_a.mlmodel")

    assert m1 is m2
    loader.assert_called_once()  # only loaded once despite two gets


def test_lru_eviction_removes_least_recently_used():
    """When the cache exceeds max_size, the oldest-accessed entry is evicted."""
    registry = _make_registry(max_size=2)

    def _side_effect(path):
        return MagicMock(name=path)

    with patch("engines.kraken._load_sender_model", side_effect=_side_effect):
        registry.get_model("/app/models/sender_a.mlmodel")
        registry.get_model("/app/models/sender_b.mlmodel")
        registry.get_model("/app/models/sender_c.mlmodel")  # should evict 'a'

    assert registry.size() == 2
    # 'a' was the least-recently-used and should be gone
    assert not registry._contains("/app/models/sender_a.mlmodel")


def test_invalidate_removes_entry_from_cache():
    """invalidate() must evict the entry so the next get reloads from disk."""
    registry = _make_registry()
    mock_model = MagicMock(name="model_x")
    with patch("engines.kraken._load_sender_model", return_value=mock_model):
        registry.get_model("/app/models/sender_x.mlmodel")
        assert registry.size() == 1

        registry.invalidate("/app/models/sender_x.mlmodel")
        assert registry.size() == 0


def test_path_outside_models_dir_raises():
    """get_model must reject paths outside /app/models/ (path traversal guard)."""
    registry = _make_registry()
    with pytest.raises(ValueError, match="not allowed"):
        registry.get_model("/etc/passwd")


def test_load_failure_does_not_cache_broken_entry():
    """A failed load must not leave a broken entry in the cache."""
    registry = _make_registry()
    with patch("engines.kraken._load_sender_model", side_effect=RuntimeError("corrupt model")):
        with pytest.raises(RuntimeError, match="corrupt model"):
            registry.get_model("/app/models/sender_bad.mlmodel")

    assert registry.size() == 0
