"""Tests for KV Cache access statistics collector"""

from __future__ import annotations

import json
import tempfile
import time
from pathlib import Path

from sagellm_kv_cache.profiling import TIME_WINDOWS, AccessStatsCollector


class TestAccessStatsCollector:
    """测试 AccessStatsCollector 类"""

    def test_init(self) -> None:
        """测试初始化"""
        collector = AccessStatsCollector()
        assert collector.total_accesses == 0
        assert collector.cache_hits == 0
        assert collector.cache_misses == 0
        assert len(collector.access_times) == 0
        assert len(collector.access_count) == 0

    def test_record_access_hit(self) -> None:
        """测试记录命中访问"""
        collector = AccessStatsCollector()
        collector.record_access("block_1", is_hit=True)

        assert collector.total_accesses == 1
        assert collector.cache_hits == 1
        assert collector.cache_misses == 0
        assert collector.access_count["block_1"] == 1
        assert len(collector.access_times["block_1"]) == 1

    def test_record_access_miss(self) -> None:
        """测试记录未命中访问"""
        collector = AccessStatsCollector()
        collector.record_access("block_1", is_hit=False)

        assert collector.total_accesses == 1
        assert collector.cache_hits == 0
        assert collector.cache_misses == 1

    def test_multiple_accesses(self) -> None:
        """测试多次访问"""
        collector = AccessStatsCollector()

        for i in range(10):
            collector.record_access(f"block_{i % 3}", is_hit=True)
            time.sleep(0.001)  # 确保时间间隔

        assert collector.total_accesses == 10
        assert len(collector.access_count) == 3
        assert collector.access_count["block_0"] == 4
        assert collector.access_count["block_1"] == 3
        assert collector.access_count["block_2"] == 3

    def test_get_access_intervals(self) -> None:
        """测试获取访问时间间隔"""
        collector = AccessStatsCollector()

        # 单次访问，无间隔
        collector.record_access("block_1", is_hit=True)
        intervals = collector.get_access_intervals("block_1")
        assert intervals == []

        # 多次访问
        time.sleep(0.01)
        collector.record_access("block_1", is_hit=True)
        time.sleep(0.01)
        collector.record_access("block_1", is_hit=True)

        intervals = collector.get_access_intervals("block_1")
        assert len(intervals) == 2
        assert all(interval > 0 for interval in intervals)

    def test_get_all_intervals(self) -> None:
        """测试获取所有访问间隔"""
        collector = AccessStatsCollector()

        for _ in range(5):
            collector.record_access("block_1", is_hit=True)
            time.sleep(0.001)

        for _ in range(3):
            collector.record_access("block_2", is_hit=True)
            time.sleep(0.001)

        all_intervals = collector.get_all_intervals()
        assert len(all_intervals) == 6  # 4 + 2

    def test_get_hit_rate(self) -> None:
        """测试命中率计算"""
        collector = AccessStatsCollector()

        # 无访问时命中率为 0
        assert collector.get_hit_rate() == 0.0

        # 记录一些访问
        for _ in range(8):
            collector.record_access("block_1", is_hit=True)

        for _ in range(2):
            collector.record_access("block_2", is_hit=False)

        assert collector.get_hit_rate() == 0.8

    def test_get_top_blocks(self) -> None:
        """测试获取访问频率最高的 block"""
        collector = AccessStatsCollector()

        # 生成不同频率的访问
        for i in range(1, 6):
            for _ in range(i):
                collector.record_access(f"block_{i}", is_hit=True)

        top_3 = collector.get_top_blocks(3)
        assert len(top_3) == 3
        assert top_3[0][0] == "block_5"
        assert top_3[0][1] == 5
        assert top_3[1][0] == "block_4"
        assert top_3[1][1] == 4

    def test_get_stats_summary(self) -> None:
        """测试统计摘要"""
        collector = AccessStatsCollector()

        for i in range(10):
            collector.record_access(f"block_{i % 3}", is_hit=i % 2 == 0)
            time.sleep(0.001)

        summary = collector.get_stats_summary()

        assert summary["total_accesses"] == 10
        assert summary["unique_blocks"] == 3
        assert "hit_rate" in summary
        assert "cache_hits" in summary
        assert "cache_misses" in summary
        assert "average_access_interval" in summary
        assert "duration_seconds" in summary
        assert "top_10_blocks" in summary

    def test_export_stats(self) -> None:
        """测试导出统计数据"""
        collector = AccessStatsCollector()

        for i in range(5):
            collector.record_access(f"block_{i % 2}", is_hit=True)
            time.sleep(0.001)

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "test_stats.json"
            collector.export_stats(output_path)

            assert output_path.exists()

            # 验证 JSON 内容
            with open(output_path, encoding="utf-8") as f:
                data = json.load(f)

            assert data["total_accesses"] == 5
            assert data["unique_blocks"] == 2
            assert "access_intervals" in data
            assert "block_details" in data

    def test_reset(self) -> None:
        """测试重置功能"""
        collector = AccessStatsCollector()

        for i in range(10):
            collector.record_access(f"block_{i}", is_hit=True)

        assert collector.total_accesses == 10

        collector.reset()

        assert collector.total_accesses == 0
        assert collector.cache_hits == 0
        assert collector.cache_misses == 0
        assert len(collector.access_times) == 0
        assert len(collector.access_count) == 0

    def test_block_details_export(self) -> None:
        """测试 block 详细信息的导出"""
        collector = AccessStatsCollector()

        # 模拟多次访问
        collector.record_access("block_1", is_hit=True)
        time.sleep(0.01)
        collector.record_access("block_1", is_hit=True)
        time.sleep(0.01)
        collector.record_access("block_2", is_hit=False)

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "test_details.json"
            collector.export_stats(output_path)

            with open(output_path, encoding="utf-8") as f:
                data = json.load(f)

            assert "block_1" in data["block_details"]
            assert "block_2" in data["block_details"]

            block_1_details = data["block_details"]["block_1"]
            assert block_1_details["access_count"] == 2
            assert "first_access" in block_1_details
            assert "last_access" in block_1_details
            assert "intervals" in block_1_details
            assert len(block_1_details["intervals"]) == 1


class TestAccessStatsColdBlocks:
    """Tests for cold-block identification feature (issue #37)."""

    def test_no_cold_blocks_when_recently_accessed(self) -> None:
        """Blocks accessed just now should NOT appear as cold."""
        collector = AccessStatsCollector()
        collector.record_access("block_1", is_hit=True)
        cold = collector.get_cold_blocks(idle_threshold_s=60.0)
        assert cold == []

    def test_cold_block_detected_with_low_threshold(self) -> None:
        """Blocks idle longer than threshold should appear in cold list."""
        collector = AccessStatsCollector()
        # Manually inject an old timestamp
        old_ts = time.time() - 120.0  # 2 minutes ago
        collector.access_times["stale_block"].append(old_ts)
        collector.access_count["stale_block"] = 1
        collector.total_accesses = 1
        collector.cache_hits = 1

        cold = collector.get_cold_blocks(idle_threshold_s=60.0)
        assert len(cold) == 1
        assert cold[0][0] == "stale_block"
        assert cold[0][1] >= 60.0

    def test_cold_blocks_sorted_by_idle_time(self) -> None:
        """Cold blocks should be sorted by idle time descending."""
        collector = AccessStatsCollector()
        now = time.time()
        collector.access_times["block_a"].append(now - 200)
        collector.access_times["block_b"].append(now - 100)
        collector.access_count["block_a"] = 1
        collector.access_count["block_b"] = 1
        collector.total_accesses = 2
        collector.cache_hits = 2

        cold = collector.get_cold_blocks(idle_threshold_s=50.0)
        assert len(cold) == 2
        assert cold[0][0] == "block_a"  # longer idle first
        assert cold[1][0] == "block_b"


class TestAccessStatsLifecycle:
    """Tests for block lifecycle distribution (issue #37)."""

    def test_empty_lifecycle_distribution(self) -> None:
        """No alloc/free events → empty distribution."""
        collector = AccessStatsCollector()
        dist = collector.get_lifecycle_distribution()
        assert dist["count"] == 0
        assert dist["min_s"] is None
        assert dist["max_s"] is None
        assert dist["mean_s"] is None
        assert dist["histogram"] == []

    def test_lifecycle_recorded_correctly(self) -> None:
        """Lifecycle of a freed block should be positive."""
        collector = AccessStatsCollector()
        collector.record_alloc("block_1")
        time.sleep(0.02)
        collector.record_free("block_1")

        dist = collector.get_lifecycle_distribution()
        assert dist["count"] == 1
        assert dist["min_s"] is not None
        assert dist["min_s"] > 0
        assert dist["max_s"] == dist["min_s"]
        assert len(dist["histogram"]) == 10

    def test_lifecycle_reset_clears_alloc_free(self) -> None:
        """reset() should clear lifecycle tables."""
        collector = AccessStatsCollector()
        collector.record_alloc("block_1")
        collector.record_free("block_1")
        collector.reset()
        assert collector.get_lifecycle_distribution()["count"] == 0

    def test_partial_lifecycle_not_counted(self) -> None:
        """Block that was allocated but not freed should not appear in distribution."""
        collector = AccessStatsCollector()
        collector.record_alloc("block_only_alloc")
        dist = collector.get_lifecycle_distribution()
        assert dist["count"] == 0


class TestAccessStatsTimeWindow:
    """Tests for time-window analysis (issue #37)."""

    def test_window_stats_empty(self) -> None:
        """Empty collector → zero accesses in any window."""
        collector = AccessStatsCollector()
        stats = collector.get_window_stats(60.0)
        assert stats["total_accesses"] == 0
        assert stats["unique_blocks"] == 0

    def test_window_stats_includes_recent_access(self) -> None:
        """Access recorded just now should appear in a 60-second window."""
        collector = AccessStatsCollector()
        collector.record_access("block_1", is_hit=True)
        stats = collector.get_window_stats(60.0)
        assert stats["total_accesses"] == 1
        assert stats["unique_blocks"] == 1

    def test_window_stats_excludes_old_access(self) -> None:
        """Access recorded 120 seconds ago should NOT appear in a 60-second window."""
        collector = AccessStatsCollector()
        old_ts = time.time() - 120.0
        collector.access_times["old_block"].append(old_ts)
        collector.access_count["old_block"] = 1
        collector.total_accesses = 1
        collector.cache_hits = 1

        stats = collector.get_window_stats(60.0)
        assert stats["total_accesses"] == 0

    def test_get_all_window_stats_keys(self) -> None:
        """get_all_window_stats() should return all preset window keys."""
        collector = AccessStatsCollector()
        all_stats = collector.get_all_window_stats()
        assert set(all_stats.keys()) == set(TIME_WINDOWS.keys())

    def test_time_windows_constant(self) -> None:
        """TIME_WINDOWS should contain expected presets."""
        assert "1min" in TIME_WINDOWS
        assert "5min" in TIME_WINDOWS
        assert "1h" in TIME_WINDOWS
        assert TIME_WINDOWS["1min"] == 60.0
        assert TIME_WINDOWS["5min"] == 300.0
        assert TIME_WINDOWS["1h"] == 3600.0


class TestAccessStatsHeatmap:
    """Tests for heatmap data generation (issue #37)."""

    def test_heatmap_empty(self) -> None:
        """Empty collector → empty heatmap."""
        collector = AccessStatsCollector()
        h = collector.get_heatmap_data()
        assert h["time_bins"] == []
        assert h["block_heatmap"] == {}

    def test_heatmap_structure(self) -> None:
        """Heatmap should have correct slot structure."""
        collector = AccessStatsCollector()
        for _ in range(5):
            collector.record_access("block_1", is_hit=True)
            time.sleep(0.002)

        h = collector.get_heatmap_data(time_slots=10)
        assert len(h["time_bins"]) == 10
        assert "block_1" in h["block_heatmap"]
        # Total counts across all slots should equal total accesses
        slot_sum = sum(h["block_heatmap"]["block_1"])
        assert slot_sum == 5

    def test_heatmap_multiple_blocks(self) -> None:
        """Heatmap should contain one row per unique block."""
        collector = AccessStatsCollector()
        for block_id in ["block_a", "block_b", "block_c"]:
            collector.record_access(block_id, is_hit=True)
        h = collector.get_heatmap_data(time_slots=5)
        assert set(h["block_heatmap"].keys()) == {"block_a", "block_b", "block_c"}


class TestAccessStatsFrequencyHistogram:
    """Tests for frequency histogram (issue #37)."""

    def test_histogram_empty(self) -> None:
        """Empty collector → empty histogram."""
        collector = AccessStatsCollector()
        assert collector.get_frequency_histogram() == []

    def test_histogram_has_correct_bins(self) -> None:
        """Histogram should return the requested number of bins."""
        collector = AccessStatsCollector()
        for i in range(1, 11):
            for _ in range(i):
                collector.record_access(f"block_{i}", is_hit=True)
        hist = collector.get_frequency_histogram(bins=5)
        assert len(hist) == 5
        for bucket in hist:
            assert "range_low" in bucket
            assert "range_high" in bucket
            assert "count" in bucket

    def test_histogram_total_count(self) -> None:
        """Sum of all histogram bucket counts should equal unique_blocks."""
        collector = AccessStatsCollector()
        for i in range(6):
            for _ in range(i + 1):
                collector.record_access(f"b_{i}", is_hit=True)
        hist = collector.get_frequency_histogram(bins=10)
        total = sum(b["count"] for b in hist)
        assert total == 6  # 6 unique blocks


class TestAccessStatsExportEnhanced:
    """Tests for enhanced JSON export with new fields (issue #37)."""

    def test_export_contains_new_fields(self) -> None:
        """Exported JSON must include window_stats, cold_blocks and heatmap."""
        collector = AccessStatsCollector()
        for i in range(8):
            collector.record_access(f"block_{i % 3}", is_hit=True)
            time.sleep(0.001)
        collector.record_alloc("block_0")
        time.sleep(0.005)
        collector.record_free("block_0")

        with tempfile.TemporaryDirectory() as tmpdir:
            path = Path(tmpdir) / "full_stats.json"
            collector.export_stats(path)

            with open(path, encoding="utf-8") as f:
                data = json.load(f)

        assert "window_stats" in data
        assert "1min" in data["window_stats"]
        assert "cold_blocks" in data
        assert "heatmap" in data
        assert "frequency_histogram" in data
        assert "lifecycle_distribution" in data

    def test_summary_contains_histogram_and_lifecycle(self) -> None:
        """get_stats_summary() must include histogram and lifecycle keys."""
        collector = AccessStatsCollector()
        collector.record_access("b1", is_hit=True)
        summary = collector.get_stats_summary()
        assert "frequency_histogram" in summary
        assert "lifecycle_distribution" in summary
