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

import time

import pytest

from sagellm_kv_cache.errors import KVAllPinnedError, KVNoVictimError
from sagellm_kv_cache.eviction import (
    EvictionManager,
    FIFOEviction,
    LRUEviction,
    PriorityEviction,
    TTLEviction,
)
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


class TestTTLEviction:
    """Test suite for TTL eviction strategy."""

    def test_ttl_basic_expired(self):
        """Test TTL eviction with expired handles."""
        policy = TTLEviction(ttl_seconds=60)

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

        # h1 expired (2 minutes ago), h2 not expired (30 seconds ago)
        h1.created_at = time.time() - 120
        h2.created_at = time.time() - 30

        # Should evict h1 first (expired)
        victims = policy.select_victim([h1, h2], 100)

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

    def test_ttl_all_expired(self):
        """Test TTL when all handles are expired."""
        policy = TTLEviction(ttl_seconds=60)

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

        # All expired at different times
        h1.created_at = time.time() - 200
        h2.created_at = time.time() - 150
        h3.created_at = time.time() - 100

        # Should evict oldest expired first
        victims = policy.select_victim([h1, h2, h3], 100)

        assert len(victims) == 1
        assert victims[0] == h1  # Oldest creation time

    def test_ttl_none_expired_fallback_to_lru(self):
        """Test TTL fallback to LRU when no handles are expired."""
        policy = TTLEviction(ttl_seconds=300)

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

        # Neither expired (within 5 minutes)
        current = time.time()
        h1.created_at = current - 60
        h2.created_at = current - 30

        # Set different access times
        h1.last_access = current - 10
        h2.last_access = current - 5

        # Should fall back to LRU (evict h1 with older access)
        victims = policy.select_victim([h1, h2], 100)

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

    def test_ttl_mixed_expired_and_fresh(self):
        """Test TTL with mix of expired and fresh handles."""
        policy = TTLEviction(ttl_seconds=60)

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

        current = time.time()

        # h1, h2 expired
        h1.created_at = current - 120
        h2.created_at = current - 100

        # h3, h4 not expired
        h3.created_at = current - 40
        h4.created_at = current - 20

        # Set access times
        h3.last_access = current - 30
        h4.last_access = current - 10

        # Need 150 tokens - should evict h1, h2 (expired) first, then h3 (oldest access)
        victims = policy.select_victim([h1, h2, h3, h4], 150)

        assert len(victims) == 3
        assert h1 in victims
        assert h2 in victims
        assert h3 in victims
        assert h4 not in victims

    def test_ttl_no_candidates(self):
        """Test TTL with no candidates."""
        policy = TTLEviction(ttl_seconds=60)

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

    def test_ttl_policy_name(self):
        """Test TTL policy returns correct enum."""
        policy = TTLEviction()
        assert policy.get_policy_name() == EvictionPolicy.TTL


class TestPriorityEviction:
    """Test suite for Priority eviction strategy."""

    def test_priority_basic(self):
        """Test basic priority eviction."""
        policy = PriorityEviction()

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

        h1.metadata = {"priority": 1}
        h2.metadata = {"priority": 5}

        # Should evict h1 (lower priority)
        victims = policy.select_victim([h1, h2], 100)

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

    def test_priority_multiple_levels(self):
        """Test priority with multiple priority levels."""
        policy = PriorityEviction()

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

        h1.metadata = {"priority": 1}
        h2.metadata = {"priority": 3}
        h3.metadata = {"priority": 5}

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

        assert len(victims) >= 2
        assert h1 in victims
        assert h2 in victims
        # h3 might be included to reach 150 tokens, but h1 and h2 should be first
        # Check that victims are ordered by priority
        assert victims[0] == h1  # Lowest priority first
        assert victims[1] == h2  # Next lowest priority

    def test_priority_default_priority(self):
        """Test handles without explicit priority get default."""
        policy = PriorityEviction(default_priority=5)

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

        h1.metadata = {"priority": 3}
        # h2 has no priority - should use default (5)

        # Should evict h1 (priority 3 < default 5)
        victims = policy.select_victim([h1, h2], 100)

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

    def test_priority_same_priority_lru_tiebreaker(self):
        """Test LRU tiebreaker for same priority."""
        policy = PriorityEviction()

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

        # Same priority
        h1.metadata = {"priority": 3}
        h2.metadata = {"priority": 3}

        # Different access times
        h1.last_access = 1.0
        h2.last_access = 2.0

        # Should evict h1 (older access)
        victims = policy.select_victim([h1, h2], 100)

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

    def test_priority_no_candidates(self):
        """Test priority with no candidates."""
        policy = PriorityEviction()

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

    def test_priority_policy_name(self):
        """Test Priority policy returns correct enum."""
        policy = PriorityEviction()
        assert policy.get_policy_name() == EvictionPolicy.PRIORITY


class TestEvictionManagerExtended:
    """Extended tests for EvictionManager with new policies."""

    def test_manager_ttl_policy(self):
        """Test manager with TTL policy."""
        manager = EvictionManager(policy=EvictionPolicy.TTL, ttl_seconds=60)

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

        h1.created_at = time.time() - 120  # Expired
        h2.created_at = time.time() - 30  # Not expired

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

        assert h1 in victims
        assert h2 not in victims

    def test_manager_priority_policy(self):
        """Test manager with Priority policy."""
        manager = EvictionManager(policy=EvictionPolicy.PRIORITY, default_priority=0)

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

        h1.metadata = {"priority": 1}
        h2.metadata = {"priority": 10}

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

        assert h1 in victims
        assert h2 not in victims

    def test_policy_hot_swap(self):
        """Test hot-swapping eviction policies."""
        manager = EvictionManager(policy=EvictionPolicy.LRU)

        # Initially LRU
        assert manager.policy == EvictionPolicy.LRU

        # Swap to FIFO
        manager.set_policy(EvictionPolicy.FIFO)
        assert manager.policy == EvictionPolicy.FIFO

        # Swap to TTL
        manager.set_policy(EvictionPolicy.TTL)
        assert manager.policy == EvictionPolicy.TTL

        # Swap to Priority
        manager.set_policy(EvictionPolicy.PRIORITY)
        assert manager.policy == EvictionPolicy.PRIORITY


class TestEvictionPerformance:
    """Performance tests for eviction policies (验收标准: evict_ms < 5ms)."""

    def test_lru_performance_100_handles(self):
        """Test LRU performance with 100 handles (Year1: < 5ms)."""
        policy = LRUEviction()

        # Create 100 handles
        handles = []
        current = time.time()
        for i in range(100):
            h = KVHandle.create(num_tokens=100)
            h.last_access = current - i
            handles.append(h)

        # Measure eviction time
        start = time.perf_counter()
        victims = policy.select_victim(handles, 1000)  # Need 10 handles
        elapsed_ms = (time.perf_counter() - start) * 1000.0

        assert len(victims) == 10
        assert elapsed_ms < 5.0, f"LRU eviction took {elapsed_ms:.2f}ms (> 5ms threshold)"

    def test_ttl_performance_100_handles(self):
        """Test TTL performance with 100 handles (Year1: < 5ms)."""
        policy = TTLEviction(ttl_seconds=60)

        # Create 100 handles (50 expired, 50 not)
        handles = []
        current = time.time()
        for i in range(50):
            h = KVHandle.create(num_tokens=100)
            h.created_at = current - 120  # Expired
            h.last_access = current - i
            handles.append(h)
        for i in range(50):
            h = KVHandle.create(num_tokens=100)
            h.created_at = current - 30  # Not expired
            h.last_access = current - i
            handles.append(h)

        # Measure eviction time
        start = time.perf_counter()
        victims = policy.select_victim(handles, 1000)  # Need 10 handles
        elapsed_ms = (time.perf_counter() - start) * 1000.0

        assert len(victims) == 10
        assert elapsed_ms < 5.0, f"TTL eviction took {elapsed_ms:.2f}ms (> 5ms threshold)"

    def test_priority_performance_100_handles(self):
        """Test Priority performance with 100 handles (Year1: < 5ms)."""
        policy = PriorityEviction()

        # Create 100 handles with varying priorities
        handles = []
        current = time.time()
        for i in range(100):
            h = KVHandle.create(num_tokens=100)
            h.metadata = {"priority": i % 10}  # Priorities 0-9
            h.last_access = current - i
            handles.append(h)

        # Measure eviction time
        start = time.perf_counter()
        victims = policy.select_victim(handles, 1000)  # Need 10 handles
        elapsed_ms = (time.perf_counter() - start) * 1000.0

        assert len(victims) == 10
        assert elapsed_ms < 5.0, f"Priority eviction took {elapsed_ms:.2f}ms (> 5ms threshold)"

    def test_fifo_performance_100_handles(self):
        """Test FIFO performance with 100 handles (Year1: < 5ms)."""
        policy = FIFOEviction()

        # Create 100 handles
        handles = []
        current = time.time()
        for i in range(100):
            h = KVHandle.create(num_tokens=100)
            h.created_at = current - i
            handles.append(h)

        # Measure eviction time
        start = time.perf_counter()
        victims = policy.select_victim(handles, 1000)  # Need 10 handles
        elapsed_ms = (time.perf_counter() - start) * 1000.0

        assert len(victims) == 10
        assert elapsed_ms < 5.0, f"FIFO eviction took {elapsed_ms:.2f}ms (> 5ms threshold)"

    def test_manager_performance_with_pinned(self):
        """Test EvictionManager performance with pinned handles."""
        manager = EvictionManager(policy=EvictionPolicy.LRU)

        # Create 100 handles, pin 20 of them
        handles = []
        current = time.time()
        for i in range(100):
            h = KVHandle.create(num_tokens=100)
            h.last_access = current - i
            if i < 20:
                h.pin()
            handles.append(h)

        # Measure eviction time
        start = time.perf_counter()
        victims = manager.select_victims(handles, 1000)  # Need 10 handles
        elapsed_ms = (time.perf_counter() - start) * 1000.0

        assert len(victims) == 10
        # None of the pinned handles should be selected
        for v in victims:
            assert not v.is_pinned
        assert elapsed_ms < 5.0, f"Manager eviction took {elapsed_ms:.2f}ms (> 5ms threshold)"
