"""Tests for Eviction Policy (Task 2.3)"""

import pytest

from sagellm_kv_cache.errors import KVAllPinnedError, KVNoVictimError
from sagellm_kv_cache.eviction import EvictionManager, FIFOEviction, LRUEviction
from sagellm_kv_cache.models import EvictionPolicy, KVHandle


class TestLRUEviction:
    """Test suite for LRU eviction strategy."""

    def test_lru_basic(self):
        """Test basic LRU eviction."""
        policy = LRUEviction()

        # Create handles with different access times
        h1 = KVHandle.create(num_tokens=100)
        h2 = KVHandle.create(num_tokens=100)
        h3 = KVHandle.create(num_tokens=100)

        h1.last_access = 1.0
        h2.last_access = 2.0
        h3.last_access = 3.0

        # Select victim - should pick h1 (oldest)
        victims = policy.select_victim([h1, h2, h3], 100)

        assert len(victims) == 1
        assert victims[0] == h1

    def test_lru_multiple_victims(self):
        """Test LRU with multiple victims needed."""
        policy = LRUEviction()

        h1 = KVHandle.create(num_tokens=100)
        h2 = KVHandle.create(num_tokens=100)
        h3 = KVHandle.create(num_tokens=100)

        h1.last_access = 1.0
        h2.last_access = 2.0
        h3.last_access = 3.0

        # Need 250 tokens - should evict h1 and h2
        victims = policy.select_victim([h1, h2, h3], 250)

        assert len(victims) == 3  # Need all 3 to get 300 tokens
        assert victims[0] == h1
        assert victims[1] == h2

    def test_lru_insufficient_tokens(self):
        """Test LRU when not enough tokens available."""
        policy = LRUEviction()

        h1 = KVHandle.create(num_tokens=50)
        h2 = KVHandle.create(num_tokens=50)

        # Need 200 tokens but only 100 available
        with pytest.raises(KVNoVictimError):
            policy.select_victim([h1, h2], 200)

    def test_lru_no_candidates(self):
        """Test LRU with no candidates."""
        policy = LRUEviction()

        with pytest.raises(KVNoVictimError):
            policy.select_victim([], 100)


class TestFIFOEviction:
    """Test suite for FIFO eviction strategy."""

    def test_fifo_basic(self):
        """Test basic FIFO eviction."""
        policy = FIFOEviction()

        h1 = KVHandle.create(num_tokens=100)
        h2 = KVHandle.create(num_tokens=100)
        h3 = KVHandle.create(num_tokens=100)

        h1.created_at = 1.0
        h2.created_at = 2.0
        h3.created_at = 3.0

        # Select victim - should pick h1 (oldest creation)
        victims = policy.select_victim([h1, h2, h3], 100)

        assert len(victims) == 1
        assert victims[0] == h1

    def test_fifo_vs_lru(self):
        """Test that FIFO uses creation time, not access time."""
        policy = FIFOEviction()

        h1 = KVHandle.create(num_tokens=100)
        h2 = KVHandle.create(num_tokens=100)

        # h1 created first but accessed later
        h1.created_at = 1.0
        h1.last_access = 10.0

        h2.created_at = 2.0
        h2.last_access = 5.0

        # FIFO should still pick h1 (older creation)
        victims = policy.select_victim([h1, h2], 100)
        assert victims[0] == h1


class TestEvictionManager:
    """Test suite for EvictionManager."""

    def test_initialization(self):
        """Test manager initialization."""
        manager = EvictionManager(policy=EvictionPolicy.LRU)
        assert manager.policy == EvictionPolicy.LRU

        manager = EvictionManager(policy=EvictionPolicy.FIFO)
        assert manager.policy == EvictionPolicy.FIFO

    def test_set_policy(self):
        """Test changing policy."""
        manager = EvictionManager(policy=EvictionPolicy.LRU)
        manager.set_policy(EvictionPolicy.FIFO)

        assert manager.policy == EvictionPolicy.FIFO

    def test_filter_pinned_handles(self):
        """Test that pinned handles are filtered out."""
        manager = EvictionManager(policy=EvictionPolicy.LRU)

        h1 = KVHandle.create(num_tokens=100)
        h2 = KVHandle.create(num_tokens=100)
        h3 = KVHandle.create(num_tokens=100)

        # Pin h1
        h1.pin()
        assert not h1.is_evictable()

        h1.last_access = 1.0
        h2.last_access = 2.0
        h3.last_access = 3.0

        # Should not select h1 even though it's oldest
        victims = manager.select_victims([h1, h2, h3], 100)

        assert h1 not in victims
        assert victims[0] == h2  # Next oldest

    def test_all_pinned_error(self):
        """Test error when all handles are pinned."""
        manager = EvictionManager()

        h1 = KVHandle.create(num_tokens=100)
        h2 = KVHandle.create(num_tokens=100)

        h1.pin()
        h2.pin()

        with pytest.raises(KVAllPinnedError):
            manager.select_victims([h1, h2], 100)

    def test_multiple_pin_counts(self):
        """Test handles with multiple pin counts."""
        manager = EvictionManager()

        h1 = KVHandle.create(num_tokens=100)
        h2 = KVHandle.create(num_tokens=100)

        # Pin h1 twice
        h1.pin()
        h1.pin()
        assert h1.pin_count == 2

        h1.last_access = 1.0
        h2.last_access = 2.0

        victims = manager.select_victims([h1, h2], 100)
        assert victims[0] == h2

        # Unpin once - still pinned
        h1.unpin()
        assert h1.pin_count == 1
        victims = manager.select_victims([h1, h2], 100)
        assert victims[0] == h2

        # Unpin again - now evictable
        h1.unpin()
        assert h1.pin_count == 0
        victims = manager.select_victims([h1, h2], 100)
        assert victims[0] == h1

    def test_repr(self):
        """Test string representation."""
        manager = EvictionManager(policy=EvictionPolicy.LRU)
        repr_str = repr(manager)

        assert "EvictionManager" in repr_str
        assert "lru" in repr_str  # Enum value is lowercase
