# -*- coding: utf-8 -*-
"""FileStateCache LRU 文件状态缓存测试（Phase 6 Tier 1-4 AQ）。

借鉴 zhikuncode-main FileStateCache.java：
- OrderedDict accessOrder=true 实现 LRU
- 大小限制：100 条 / 25MB 任一超限淘汰
- 路径规范化（os.path.realpath）作为 key
- 存储 (mtime_ns, size, content) 三元组
- 读取时比对 mtime_ns + size，未变则命中
"""
import os
import threading
import time
from pathlib import Path

import pytest

from abyss.tools.file_cache import FileStateCache, get_default_cache


@pytest.fixture
def cache() -> FileStateCache:
    """返回独立的 FileStateCache 实例（不影响全局单例）。"""
    return FileStateCache()


@pytest.fixture
def make_file(tmp_path: Path):
    """工厂 fixture：创建文件并返回路径。"""
    def _make(name: str, content: str = "hello") -> str:
        path = tmp_path / name
        path.write_text(content, encoding="utf-8")
        return str(path)
    return _make


class TestFileStateCacheBasic:
    """基本功能测试。"""

    def test_get_returns_none_for_uncached_file(self, cache, make_file):
        path = make_file("a.txt")
        assert cache.get(path) is None

    def test_put_then_get_returns_content(self, cache, make_file):
        path = make_file("a.txt", "hello world")
        cache.put(path, "hello world")
        assert cache.get(path) == "hello world"

    def test_put_unicode_content(self, cache, make_file):
        path = make_file("u.txt", "中文 🚀")
        cache.put(path, "中文 🚀")
        assert cache.get(path) == "中文 🚀"

    def test_put_empty_content(self, cache, make_file):
        path = make_file("empty.txt", "")
        cache.put(path, "")
        assert cache.get(path) == ""


class TestFileStateCacheInvalidation:
    """失效逻辑测试。"""

    def test_get_invalidates_on_mtime_change(self, cache, make_file, tmp_path):
        """文件被外部修改后，mtime 变化，缓存应失效。"""
        path = make_file("a.txt", "original")
        cache.put(path, "original")
        # 确保命中
        assert cache.get(path) == "original"
        # 修改文件（需要确保 mtime 变化）
        time.sleep(0.01)
        Path(path).write_text("modified", encoding="utf-8")
        # 应失效返回 None
        assert cache.get(path) is None

    def test_get_invalidates_on_size_change(self, cache, make_file, tmp_path):
        """文件大小变化后缓存失效。"""
        path = make_file("a.txt", "short")
        cache.put(path, "short")
        assert cache.get(path) == "short"
        # 写入更长的内容（不一定 mtime 变，但 size 必变）
        Path(path).write_text("this is much longer content", encoding="utf-8")
        assert cache.get(path) is None

    def test_invalidate_removes_entry(self, cache, make_file):
        path = make_file("a.txt", "content")
        cache.put(path, "content")
        assert cache.get(path) == "content"
        cache.invalidate(path)
        assert cache.get(path) is None

    def test_invalidate_nonexistent_path_no_error(self, cache):
        # 不存在的路径不应抛异常
        cache.invalidate("/nonexistent/path.txt")

    def test_put_overrides_existing_entry(self, cache, make_file):
        path = make_file("a.txt", "first")
        cache.put(path, "first")
        cache.put(path, "second")
        assert cache.get(path) == "second"

    def test_clear_removes_all_entries(self, cache, make_file):
        for i in range(5):
            p = make_file(f"f{i}.txt", f"content-{i}")
            cache.put(p, f"content-{i}")
        cache.clear()
        for i in range(5):
            p = make_file(f"f{i}.txt", f"content-{i}")
            assert cache.get(p) is None


class TestFileStateCacheLRUEviction:
    """LRU 淘汰策略测试。"""

    def test_lru_evicts_oldest_when_max_entries_exceeded(self, tmp_path):
        """超过最大条目数时淘汰最旧条目。"""
        cache = FileStateCache(max_entries=3)
        paths = []
        for i in range(5):
            p = str(tmp_path / f"f{i}.txt")
            Path(p).write_text(f"content-{i}", encoding="utf-8")
            cache.put(p, f"content-{i}")
            paths.append(p)
        # 应只剩最后 3 个
        assert cache.get(paths[0]) is None
        assert cache.get(paths[1]) is None
        assert cache.get(paths[2]) == "content-2"
        assert cache.get(paths[3]) == "content-3"
        assert cache.get(paths[4]) == "content-4"

    def test_lru_evicts_oldest_when_max_bytes_exceeded(self, tmp_path):
        """超过最大字节数时淘汰最旧条目。"""
        cache = FileStateCache(max_total_bytes=100)
        # 每个 50 字节，2 个就达上限
        for i in range(5):
            p = str(tmp_path / f"f{i}.txt")
            content = f"x{ i }" + "a" * 45  # 约 47 字节
            Path(p).write_text(content, encoding="utf-8")
            cache.put(p, content)
        # 总大小已超 100 字节，应淘汰早期条目
        # 验证至少一个早期条目被淘汰
        early_p = str(tmp_path / "f0.txt")
        # 由于 LRU 淘汰，f0 应已被淘汰
        # 注意：具体淘汰数取决于实现，但至少淘汰一个
        # 关键是不破坏缓存结构
        for i in range(5):
            p = str(tmp_path / f"f{i}.txt")
            result = cache.get(p)
            # 结果要么是 None（被淘汰），要么是原内容（未淘汰）
            assert result is None or "a" * 45 in result

    def test_lru_promotes_on_get(self, tmp_path):
        """get 命中后将条目移到末尾（最近使用）。"""
        cache = FileStateCache(max_entries=3)
        # 填满 3 个
        for i in range(3):
            p = str(tmp_path / f"f{i}.txt")
            Path(p).write_text(f"c{i}", encoding="utf-8")
            cache.put(p, f"c{i}")
        # 访问 f0 使其成为最近使用
        cache.get(str(tmp_path / "f0.txt"))
        # 添加 f3，应淘汰最久未使用（f1）
        p3 = str(tmp_path / "f3.txt")
        Path(p3).write_text("c3", encoding="utf-8")
        cache.put(p3, "c3")
        # f1 应被淘汰
        assert cache.get(str(tmp_path / "f1.txt")) is None
        # f0 仍存在（最近被访问）
        assert cache.get(str(tmp_path / "f0.txt")) == "c0"

    def test_default_max_entries_is_100(self):
        c = FileStateCache()
        assert c.MAX_ENTRIES == 100

    def test_default_max_total_bytes_is_25mb(self):
        c = FileStateCache()
        assert c.MAX_TOTAL_BYTES == 25 * 1024 * 1024


class TestFileStateCachePathNormalization:
    """路径规范化测试。"""

    def test_realpath_normalization(self, cache, make_file, tmp_path):
        """通过 ./ 和 ../ 引用同一文件应命中同一缓存项。"""
        path = make_file("a.txt", "content")
        cache.put(path, "content")
        # 通过相对路径引用（需在 tmp_path 中切换工作目录）
        cwd = os.getcwd()
        try:
            os.chdir(str(tmp_path))
            # 直接路径命中
            assert cache.get(path) == "content"
            # 通过 ./ 引用
            assert cache.get("./a.txt") == "content"
            # 通过 a.txt 引用
            assert cache.get("a.txt") == "content"
        finally:
            os.chdir(cwd)

    def test_different_files_dont_share_cache(self, cache, make_file):
        p1 = make_file("a.txt", "content-a")
        p2 = make_file("b.txt", "content-b")
        cache.put(p1, "content-a")
        cache.put(p2, "content-b")
        assert cache.get(p1) == "content-a"
        assert cache.get(p2) == "content-b"


class TestFileStateCacheConcurrency:
    """并发安全测试。"""

    def test_concurrent_access_thread_safe(self, cache, tmp_path):
        """多线程并发读写不破坏缓存结构。"""
        paths = []
        for i in range(20):
            p = str(tmp_path / f"f{i}.txt")
            Path(p).write_text(f"content-{i}", encoding="utf-8")
            paths.append(p)

        errors = []

        def reader(idx):
            try:
                for _ in range(50):
                    cache.get(paths[idx % len(paths)])
            except Exception as e:
                errors.append(e)

        def writer(idx):
            try:
                for _ in range(50):
                    cache.put(paths[idx % len(paths)], f"c-{idx}")
            except Exception as e:
                errors.append(e)

        threads = []
        for i in range(10):
            threads.append(threading.Thread(target=reader, args=(i,)))
            threads.append(threading.Thread(target=writer, args=(i,)))
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0, f"并发错误: {errors}"

    def test_concurrent_put_same_path_safe(self, cache, make_file):
        """多线程同时 put 同一路径不损坏缓存。"""
        path = make_file("a.txt", "init")
        errors = []

        def worker(idx):
            try:
                for _ in range(20):
                    cache.put(path, f"content-{idx}")
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=worker, args=(i,)) for i in range(5)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0, f"并发错误: {errors}"
        # 最终内容应是某次写入的值
        result = cache.get(path)
        assert result.startswith("content-")


class TestFileStateCacheEdgeCases:
    """边界情况测试。"""

    def test_get_nonexistent_file_returns_none(self, cache):
        """get 不存在的文件路径应返回 None（不抛异常）。"""
        assert cache.get("/nonexistent/path/file.txt") is None

    def test_put_nonexistent_file_no_error(self, cache):
        """put 不存在的文件路径不应抛异常（虽然无法记录 mtime）。"""
        cache.put("/nonexistent/path/file.txt", "content")
        # 后续 get 应返回 None（因为文件不存在）
        assert cache.get("/nonexistent/path/file.txt") is None

    def test_put_after_invalidate_works(self, cache, make_file):
        path = make_file("a.txt", "first")
        cache.put(path, "first")
        cache.invalidate(path)
        cache.put(path, "second")
        assert cache.get(path) == "second"


class TestFileStateCacheDefault:
    """模块级默认单例测试。"""

    def test_get_default_cache_returns_same_instance(self):
        c1 = get_default_cache()
        c2 = get_default_cache()
        assert c1 is c2

    def test_default_cache_can_put_and_get(self, make_file):
        cache = get_default_cache()
        path = make_file("default_test.txt", "default content")
        cache.clear()  # 清空避免污染
        cache.put(path, "default content")
        assert cache.get(path) == "default content"
        cache.clear()  # 清理
