"""Tests for S3-FIFO cache policy."""

import pytest

from s3fifo._s3fifo import S3FIFOCache, _Entry


class TestS3FIFOCacheBasic:
    def test_put_and_get(self):
        cache = S3FIFOCache(10)
        cache.put("a", 1)
        assert cache.get("a") == 1

    def test_contains(self):
        cache = S3FIFOCache(10)
        cache.put("a", 1)
        assert "a" in cache
        assert "b" not in cache

    def test_len(self):
        cache = S3FIFOCache(10)
        assert len(cache) == 0
        cache.put("a", 1)
        assert len(cache) == 1
        cache.put("b", 2)
        assert len(cache) == 2

    def test_miss_returns_none(self):
        cache = S3FIFOCache(10)
        assert cache.get("nonexistent") is None

    def test_remove(self):
        cache = S3FIFOCache(10)
        cache.put("a", 1)
        assert cache.remove("a") is True
        assert cache.get("a") is None
        assert cache.remove("a") is False

    def test_clear(self):
        cache = S3FIFOCache(10)
        cache.put("a", 1)
        cache.put("b", 2)
        cache.clear()
        assert len(cache) == 0
        assert cache.get("a") is None

    def test_put_existing_key_does_not_evict(self):
        cache = S3FIFOCache(2)
        cache.put("a", 1)
        cache.put("b", 2)
        cache.put("a", 3)
        assert len(cache) == 2
        assert cache.get("a") == 3
        assert cache.get("b") == 2

    def test_remove_without_ghost(self):
        cache = S3FIFOCache(5)
        cache.put("a", 1)
        for i in range(6):
            cache.put(i, i)
        assert cache.remove("a", include_ghost=False) is False
        assert cache.remove("a") is True

    def test_custom_configuration(self):
        cache = S3FIFOCache(
            4,
            small_size_ratio=0.5,
            ghost_size_ratio=0.5,
            move_to_main_threshold=1,
        )
        cache.put("a", 1)
        cache.put("b", 2)
        cache.get("a")
        cache.put("c", 3)
        cache.put("d", 4)
        cache.put("e", 5)
        assert cache.get("a") == 1

    @pytest.mark.parametrize(
        ("small_size_ratio", "ghost_size_ratio", "move_to_main_threshold"),
        [
            (-0.1, 0.9, 2),
            (1.1, 0.9, 2),
            (0.1, -0.1, 2),
            (0.1, 0.9, 0),
        ],
    )
    def test_invalid_configuration(
        self,
        small_size_ratio: float,
        ghost_size_ratio: float,
        move_to_main_threshold: int,
    ):
        with pytest.raises(ValueError):
            S3FIFOCache(
                10,
                small_size_ratio=small_size_ratio,
                ghost_size_ratio=ghost_size_ratio,
                move_to_main_threshold=move_to_main_threshold,
            )


class TestS3FIFOEviction:
    def test_evicts_when_full(self):
        cache = S3FIFOCache(3)
        for i in range(4):
            cache.put(i, i * 10)
        assert len(cache) == 3

    def test_frequently_accessed_survives_small_eviction(self):
        cache = S3FIFOCache(10)
        # small_cap=1, main_cap=9
        cache.put("a", 1)
        # Access "a" twice to meet move_to_main_threshold
        cache.get("a")
        cache.get("a")
        # Fill up to force eviction from small
        for i in range(10):
            cache.put(i, i)
        # "a" should have moved to main and still be there
        assert cache.get("a") == 1

    def test_infrequent_evicted_from_small(self):
        cache = S3FIFOCache(10)
        # small_cap=1, main_cap=9
        cache.put("a", 1)
        # Don't access "a" again — freq stays 0
        # Fill cache: items 0-8 go to main (small full, cache not full)
        # Item 9 triggers eviction, "a" evicted from small to ghost
        for i in range(11):
            cache.put(i, i)
        # "a" should have been evicted (freq < threshold)
        assert cache.get("a") is None

    def test_ghost_promotes_to_main(self):
        cache = S3FIFOCache(5)
        # small_cap=1, main_cap=4, ghost_cap=4
        cache.put("a", 1)
        # Fill to force eviction of "a" from small into ghost
        # Items 0-3 go to main (small full, not yet evicted), item 4 triggers eviction
        for i in range(6):
            cache.put(i, i)
        assert "a" not in cache
        # Re-insert "a" — should hit ghost and go to main
        cache.put("a", 100)
        assert cache.get("a") == 100

    def test_main_clock_eviction(self):
        # Items with freq>=1 get reinserted with decayed freq
        cache = S3FIFOCache(3)
        cache.put("a", 1)
        cache.get("a")  # freq=1 in small
        cache.get("a")  # freq=2, will move to main on eviction
        cache.put("b", 2)
        cache.put("c", 3)
        cache.put("d", 4)  # triggers eviction
        # Cache should still have 3 items
        assert len(cache) == 3

    def test_maxsize_one(self):
        cache = S3FIFOCache(1)
        cache.put("a", 1)
        assert cache.get("a") == 1
        cache.put("b", 2)
        assert len(cache) == 1
        assert cache.get("b") == 2
        assert cache.get("a") is None

    def test_main_clock_rotates_until_zero_before_evict(self):
        cache = S3FIFOCache(3)

        # Build a deterministic main queue state:
        # two hot objects in main, both with high freq.
        cache._small.clear()
        cache._main.clear()
        cache._main["a"] = _Entry(value=1, freq=3)
        cache._main["b"] = _Entry(value=2, freq=3)

        cache._evict_main()

        # The first entry should only be evicted after several rotations.
        # The survivor must be decayed to zero, matching s3fifo.c behavior.
        assert "a" not in cache._main
        assert cache._main["b"].freq == 0
