# -*- coding: utf-8 -*-
"""
KnowledgeLoader 测试
覆盖：多目录扫描、frontmatter 解析、关键词匹配、两级同名项目级优先、
      解析失败跳过、build_index_lines 格式、list_all 返回所有条目。
"""
from pathlib import Path

import pytest

from abyss.knowledge import Knowledge, KnowledgeLoader, create_default_loader


def _write_md(path: Path, name: str, description: str,
              tags: list = None, level: str = "", body: str = "正文",
              category: str = "") -> None:
    """写一个 frontmatter + 正文的 .md 文件到 path。"""
    tags = tags or []
    tags_line = f"[{', '.join(tags)}]" if tags else "[]"
    level_line = f"level: {level}" if level else ""
    category_line = f"category: {category}" if category else ""
    fm = "---\n"
    fm += f"name: {name}\n"
    fm += f"description: {description}\n"
    fm += f"tags: {tags_line}\n"
    if level_line:
        fm += f"{level_line}\n"
    if category_line:
        fm += f"{category_line}\n"
    fm += "---\n"
    fm += body
    path.write_text(fm, encoding="utf-8")


def _write_no_frontmatter(path: Path, body: str = "无 frontmatter 内容") -> None:
    """写一个无 frontmatter 的 .md 文件。"""
    path.write_text(body, encoding="utf-8")


class TestDiscover:
    """discover 多目录扫描测试。"""

    def test_scan_both_dirs(self, tmp_path):
        global_dir = tmp_path / "global"
        project_dir = tmp_path / "project"
        global_dir.mkdir()
        project_dir.mkdir()
        _write_md(global_dir / "a.md", "knowledge-a", "全局知识 A")
        _write_md(project_dir / "b.md", "knowledge-b", "项目知识 B")

        loader = KnowledgeLoader({"global": global_dir, "project": project_dir})
        loader.discover()

        assert loader.get("knowledge-a") is not None
        assert loader.get("knowledge-b") is not None

    def test_project_none_only_global(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(global_dir / "a.md", "knowledge-a", "全局知识 A")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        assert loader.get("knowledge-a") is not None

    def test_nonexistent_dir_skipped(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(global_dir / "a.md", "knowledge-a", "全局知识 A")
        nonexistent = tmp_path / "nonexistent"

        loader = KnowledgeLoader({"global": global_dir, "project": nonexistent})
        loader.discover()  # 不应抛异常

        assert loader.get("knowledge-a") is not None


class TestFrontmatterParse:
    """frontmatter 解析测试。"""

    def test_parse_name_description_tags(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(
            global_dir / "react.md",
            "react-hooks-stale-closure",
            "React Hooks 闭包陷阱的根因与三种修复方案",
            tags=["react", "hooks", "bug"],
            level="global",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        k = loader.get("react-hooks-stale-closure")
        assert k is not None
        assert k.name == "react-hooks-stale-closure"
        assert k.description == "React Hooks 闭包陷阱的根因与三种修复方案"
        assert k.tags == ["react", "hooks", "bug"]
        assert k.content == "正文"
        assert k.level == "global"

    def test_level_overridden_by_directory(self, tmp_path):
        """frontmatter 的 level 字段被目录级别覆盖。"""
        global_dir = tmp_path / "global"
        project_dir = tmp_path / "project"
        global_dir.mkdir()
        project_dir.mkdir()
        # 即便 frontmatter 写 level: project，全局目录下的也应被覆盖为 global
        _write_md(
            global_dir / "a.md",
            "knowledge-a",
            "知识 A",
            level="project",
        )
        _write_md(
            project_dir / "b.md",
            "knowledge-b",
            "知识 B",
            level="global",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": project_dir})
        loader.discover()

        assert loader.get("knowledge-a").level == "global"
        assert loader.get("knowledge-b").level == "project"


class TestMatchByKeyword:
    """关键词匹配测试。"""

    def test_match_react_hooks(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(
            global_dir / "react.md",
            "react-hooks-stale-closure",
            "react hooks 闭包陷阱的根因与修复方案",
            tags=["react", "hooks"],
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        matched = loader.match_by_keyword("react hooks 问题")
        assert len(matched) >= 1
        assert matched[0].name == "react-hooks-stale-closure"

    def test_match_no_hit_returns_empty(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(
            global_dir / "react.md",
            "react-hooks",
            "react hooks 闭包",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        matched = loader.match_by_keyword("今天天气真好")
        assert matched == []

    def test_match_sorted_by_hit_count_desc(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(global_dir / "a.md", "k-a", "react hooks 闭包")
        _write_md(global_dir / "b.md", "k-b", "react")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        matched = loader.match_by_keyword("react hooks 闭包")
        # 命中数多的排前面（k-a 命中 react+hooks+闭包 三个，k-b 命中 react 一个）
        assert matched[0].name == "k-a"


class TestProjectOverridesGlobal:
    """两级同名项目级优先测试。"""

    def test_same_name_project_wins(self, tmp_path):
        global_dir = tmp_path / "global"
        project_dir = tmp_path / "project"
        global_dir.mkdir()
        project_dir.mkdir()
        _write_md(
            global_dir / "react.md",
            "react-hooks",
            "全局级描述",
            body="全局级正文",
        )
        _write_md(
            project_dir / "react.md",
            "react-hooks",
            "项目级描述",
            body="项目级正文",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": project_dir})
        loader.discover()

        k = loader.get("react-hooks")
        assert k is not None
        assert k.level == "project"
        assert k.content == "项目级正文"
        assert k.description == "项目级描述"


class TestParseFailureSkip:
    """解析失败跳过测试。"""

    def test_no_frontmatter_skipped(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(global_dir / "good.md", "good-k", "好的知识")
        _write_no_frontmatter(global_dir / "bad.md", "没有 frontmatter 的文件")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()  # 不应抛异常

        assert loader.get("good-k") is not None
        assert loader.get("bad") is None
        assert loader.get("bad.md") is None

    def test_missing_name_skipped(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        # 缺 name 字段
        global_dir.joinpath("noname.md").write_text(
            "---\ndescription: 缺 name\n---\n正文",
            encoding="utf-8",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        assert loader.list_all() == []

    def test_missing_description_skipped(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        global_dir.joinpath("nodesc.md").write_text(
            "---\nname: no-desc\n---\n正文",
            encoding="utf-8",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        assert loader.list_all() == []


class TestBuildIndexLines:
    """build_index_lines 格式测试。"""

    def test_empty_returns_empty(self, tmp_path):
        loader = KnowledgeLoader({"global": tmp_path, "project": None})
        assert loader.build_index_lines([]) == ""

    def test_global_prefix(self, tmp_path):
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(global_dir / "a.md", "k-a", "全局知识 A")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        k = loader.get("k-a")

        line = loader.build_index_lines([k])
        assert line == "[G][exp] k-a: 全局知识 A"

    def test_project_prefix(self, tmp_path):
        project_dir = tmp_path / "project"
        project_dir.mkdir()
        _write_md(project_dir / "b.md", "k-b", "项目知识 B")

        loader = KnowledgeLoader({"global": None, "project": project_dir})
        loader.discover()
        k = loader.get("k-b")

        line = loader.build_index_lines([k])
        assert line == "[P][exp] k-b: 项目知识 B"

    def test_multi_lines_joined_by_newline(self, tmp_path):
        global_dir = tmp_path / "global"
        project_dir = tmp_path / "project"
        global_dir.mkdir()
        project_dir.mkdir()
        _write_md(global_dir / "a.md", "k-a", "全局知识")
        _write_md(project_dir / "b.md", "k-b", "项目知识")

        loader = KnowledgeLoader({"global": global_dir, "project": project_dir})
        loader.discover()

        lines = loader.build_index_lines(loader.list_all())
        assert "[G][exp] k-a: 全局知识" in lines
        assert "[P][exp] k-b: 项目知识" in lines
        assert lines.count("\n") == 1


class TestListAll:
    """list_all 返回所有条目测试。"""

    def test_list_all_count(self, tmp_path):
        global_dir = tmp_path / "global"
        project_dir = tmp_path / "project"
        global_dir.mkdir()
        project_dir.mkdir()
        _write_md(global_dir / "a.md", "k-a", "全局 A")
        _write_md(global_dir / "b.md", "k-b", "全局 B")
        _write_md(project_dir / "c.md", "k-c", "项目 C")

        loader = KnowledgeLoader({"global": global_dir, "project": project_dir})
        loader.discover()

        all_k = loader.list_all()
        assert len(all_k) == 3
        names = {k.name for k in all_k}
        assert names == {"k-a", "k-b", "k-c"}

    def test_list_all_empty_before_discover(self, tmp_path):
        loader = KnowledgeLoader({"global": tmp_path, "project": None})
        assert loader.list_all() == []


class TestCreateDefaultLoader:
    """create_default_loader 工厂测试。"""

    def test_factory_returns_loader_with_correct_dirs(self, tmp_path, monkeypatch):
        # mock Path.home() 到临时目录
        monkeypatch.setattr(Path, "home", lambda: tmp_path)
        loader = create_default_loader(str(tmp_path / "work"))

        assert isinstance(loader, KnowledgeLoader)
        assert loader.knowledge_dirs["global"] == tmp_path / ".abyss" / "knowledge"
        assert loader.knowledge_dirs["project"] == tmp_path / "work" / ".knowledge"


def _write_md_categorized(base_dir: Path, category_dir: str, filename: str,
                          name: str, description: str,
                          tags: list = None, body: str = "正文",
                          category: str = None) -> Path:
    """写一个带 category 的 .md 文件到 base_dir/category_dir/filename。
    返回文件路径。
    """
    cat_dir = base_dir / category_dir
    cat_dir.mkdir(parents=True, exist_ok=True)
    file_path = cat_dir / filename
    tags = tags or []
    tags_line = f"[{', '.join(tags)}]" if tags else "[]"
    fm = "---\n"
    fm += f"name: {name}\n"
    fm += f"description: {description}\n"
    fm += f"tags: {tags_line}\n"
    if category:
        fm += f"category: {category}\n"
    fm += "---\n"
    fm += body
    file_path.write_text(fm, encoding="utf-8")
    return file_path


class TestCategorySubdirScan:
    """分类子目录扫描测试。"""

    def test_scan_preference_subdir(self, tmp_path):
        """preferences/ 子目录下的知识被正确扫描。"""
        global_dir = tmp_path / "global"
        _write_md_categorized(
            global_dir, "preferences", "pref.md",
            "user-likes-type-hints", "用户喜欢用 type hints",
            tags=["python", "typing"], category="preference",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        k = loader.get("user-likes-type-hints")
        assert k is not None
        assert k.category == "preference"
        assert k.level == "global"

    def test_scan_bugfix_subdir(self, tmp_path):
        """bugfixes/ 子目录下的知识被正确扫描。"""
        project_dir = tmp_path / "project"
        _write_md_categorized(
            project_dir, "bugfixes", "bug1.md",
            "spinner-overwrite-bug", "spinner 光标覆盖 bug 的修复方法",
            tags=["spinner", "cursor"], category="bugfix",
        )

        loader = KnowledgeLoader({"global": None, "project": project_dir})
        loader.discover()

        k = loader.get("spinner-overwrite-bug")
        assert k is not None
        assert k.category == "bugfix"
        assert k.level == "project"

    def test_scan_all_four_categories(self, tmp_path):
        """四个分类子目录都能被扫描。"""
        global_dir = tmp_path / "global"
        project_dir = tmp_path / "project"
        _write_md_categorized(global_dir, "preferences", "p1.md", "pref-1", "偏好1", category="preference")
        _write_md_categorized(global_dir, "experiences", "e1.md", "exp-1", "经验1", category="experience")
        _write_md_categorized(global_dir, "tools", "t1.md", "tool-1", "工具1", category="tool")
        _write_md_categorized(project_dir, "bugfixes", "b1.md", "bug-1", "修复1", category="bugfix")

        loader = KnowledgeLoader({"global": global_dir, "project": project_dir})
        loader.discover()

        assert loader.get("pref-1").category == "preference"
        assert loader.get("exp-1").category == "experience"
        assert loader.get("tool-1").category == "tool"
        assert loader.get("bug-1").category == "bugfix"


class TestBackwardCompatRootMd:
    """向后兼容：根目录下的旧 .md 文件默认归类为 experience。"""

    def test_root_md_defaults_to_experience(self, tmp_path):
        """无 category 字段的根目录 .md 被归类为 experience。"""
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(global_dir / "old.md", "old-knowledge", "旧知识无分类")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        k = loader.get("old-knowledge")
        assert k is not None
        assert k.category == "experience"

    def test_root_md_with_category_field(self, tmp_path):
        """根目录 .md 但 frontmatter 有 category 字段时，按字段分类。"""
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        global_dir.joinpath("pref.md").write_text(
            "---\nname: root-pref\ndescription: 根目录偏好\ncategory: preference\ntags: []\n---\n正文",
            encoding="utf-8",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        k = loader.get("root-pref")
        assert k is not None
        assert k.category == "preference"


class TestTagsMatch:
    """tags 参与匹配测试。"""

    def test_tags_boost_match_score(self, tmp_path):
        """tags 命中增加匹配权重，使 tags 命中多的排前面。"""
        global_dir = tmp_path / "global"
        _write_md_categorized(
            global_dir, "experiences", "a.md", "k-a",
            "通用编码", tags=["python", "async"],
            category="experience",
        )
        _write_md_categorized(
            global_dir, "experiences", "b.md", "k-b",
            "通用编码", tags=["python", "async", "testing"],
            category="experience",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        # 两个 description 相同，但 k-b 的 tags 多命中 testing
        matched = loader.match_by_keyword("通用编码 python async testing")
        assert len(matched) >= 2
        # k-b 的 tags 命中更多，应排前面
        assert matched[0].name == "k-b"

    def test_tags_only_match_when_desc_misses(self, tmp_path):
        """description 不命中但 tags 命中时也能匹配。"""
        global_dir = tmp_path / "global"
        _write_md_categorized(
            global_dir, "tools", "t1.md", "tool-1",
            "某工具说明", tags=["file_write", "cursor"],
            category="tool",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        # "file_write" 不在 description 中，但在 tags 中
        matched = loader.match_by_keyword("file_write cursor")
        assert len(matched) >= 1
        assert matched[0].name == "tool-1"


class TestCategoryFilter:
    """match_by_keyword 的 category 过滤测试。"""

    def test_filter_by_category(self, tmp_path):
        """指定 category 只返回该类型的知识。"""
        global_dir = tmp_path / "global"
        _write_md_categorized(global_dir, "preferences", "p1.md", "pref-1", "python 偏好", category="preference")
        _write_md_categorized(global_dir, "experiences", "e1.md", "exp-1", "python 经验", category="experience")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        matched = loader.match_by_keyword("python", category="preference")
        assert len(matched) == 1
        assert matched[0].name == "pref-1"
        assert matched[0].category == "preference"

    def test_no_filter_returns_all_categories(self, tmp_path):
        """不指定 category 返回所有类型的知识。"""
        global_dir = tmp_path / "global"
        _write_md_categorized(global_dir, "preferences", "p1.md", "pref-1", "python 偏好", category="preference")
        _write_md_categorized(global_dir, "experiences", "e1.md", "exp-1", "python 经验", category="experience")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()

        matched = loader.match_by_keyword("python")
        assert len(matched) == 2


class TestBuildIndexLinesWithCategory:
    """build_index_lines 分类前缀测试。"""

    def test_index_with_category_prefix(self, tmp_path):
        """索引行包含分类前缀：[G][pref] name: desc"""
        global_dir = tmp_path / "global"
        _write_md_categorized(global_dir, "preferences", "p1.md", "pref-1", "偏好", category="preference")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        k = loader.get("pref-1")

        line = loader.build_index_lines([k])
        assert "[G][pref]" in line
        assert "pref-1" in line

    def test_index_bugfix_prefix(self, tmp_path):
        """bugfix 类型的索引行前缀为 [bug]"""
        project_dir = tmp_path / "project"
        _write_md_categorized(project_dir, "bugfixes", "b1.md", "bug-1", "bug修复", category="bugfix")

        loader = KnowledgeLoader({"global": None, "project": project_dir})
        loader.discover()
        k = loader.get("bug-1")

        line = loader.build_index_lines([k])
        assert "[P][bug]" in line

    def test_index_experience_prefix(self, tmp_path):
        """experience 类型的索引行前缀为 [exp]"""
        global_dir = tmp_path / "global"
        _write_md_categorized(global_dir, "experiences", "e1.md", "exp-1", "经验", category="experience")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        k = loader.get("exp-1")

        line = loader.build_index_lines([k])
        assert "[G][exp]" in line

    def test_index_tool_prefix(self, tmp_path):
        """tool 类型的索引行前缀为 [tool]"""
        global_dir = tmp_path / "global"
        _write_md_categorized(global_dir, "tools", "t1.md", "tool-1", "工具", category="tool")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        k = loader.get("tool-1")

        line = loader.build_index_lines([k])
        assert "[G][tool]" in line


class TestKnowledgeLifecycleFields:
    """Knowledge 生命周期字段测试。"""

    def test_default_lifecycle_fields(self, tmp_path):
        """新建 Knowledge 的生命周期字段有默认值。"""
        global_dir = tmp_path / "global"
        _write_md_categorized(global_dir, "preferences", "p1.md", "pref-1", "偏好", category="preference")

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        k = loader.get("pref-1")

        assert k.usage_count == 0
        assert k.last_used_at is None
        assert k.pinned is False

    def test_frontmatter_lifecycle_fields_parsed(self, tmp_path):
        """frontmatter 中的生命周期字段被正确解析。"""
        global_dir = tmp_path / "global"
        global_dir.mkdir(parents=True, exist_ok=True)
        (global_dir / "p1.md").write_text(
            "---\n"
            "name: pref-1\n"
            "description: 偏好\n"
            "category: preference\n"
            "tags: [python]\n"
            "usage_count: 5\n"
            "pinned: true\n"
            "---\n正文",
            encoding="utf-8",
        )

        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        k = loader.get("pref-1")

        assert k.usage_count == 5
        assert k.pinned is True
