# -*- coding: utf-8 -*-
"""
knowledge_fetch / knowledge_save 工具测试
覆盖：
  fetch：命中、未命中、host=None
  save：新建、覆盖、level 路径、失败不抛
"""
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from abyss.knowledge import Knowledge
from abyss.tools.knowledge_fetch import KnowledgeFetchTool
from abyss.tools.knowledge_save import KnowledgeSaveTool


def _make_knowledge(name: str = "react-hooks") -> Knowledge:
    """构造测试用 Knowledge 对象。"""
    return Knowledge(
        name=name,
        description="react hooks 闭包陷阱",
        content="# React Hooks\n闭包陷阱正文",
        level="global",
        path=Path("/tmp/fake.md"),
        tags=["react", "hooks", "bug"],
    )


def _make_fake_host(hit: bool = True) -> MagicMock:
    """构造 fake host：hit=True 返回 Knowledge，False 返回 None。"""
    host = MagicMock()
    host.get_knowledge.return_value = _make_knowledge() if hit else None
    return host


class TestKnowledgeFetchHit:
    """knowledge_fetch 命中场景。"""

    def test_returns_success_with_content(self):
        """host 命中时返回 success=True 及完整正文。"""
        host = _make_fake_host(hit=True)
        tool = KnowledgeFetchTool(host=host)
        result = tool.execute(name="react-hooks")

        assert result["success"] is True
        assert result["name"] == "react-hooks"
        assert result["level"] == "global"
        assert result["category"] == "experience"
        assert "闭包陷阱" in result["content"]
        assert result["tags"] == ["react", "hooks", "bug"]

    def test_bumps_use_on_fetch(self):
        """fetch 成功时调用 host.bump_knowledge_use 记录使用。"""
        host = _make_fake_host(hit=True)
        tool = KnowledgeFetchTool(host=host)
        tool.execute(name="react-hooks")

        host.bump_knowledge_use.assert_called_once_with("react-hooks")


class TestKnowledgeFetchMiss:
    """knowledge_fetch 未命中场景。"""

    def test_returns_failure_when_not_found(self):
        """host.get_knowledge 返回 None 时，success=False 且错误含名称。"""
        tool = KnowledgeFetchTool(host=_make_fake_host(hit=False))
        result = tool.execute(name="nonexistent")

        assert result["success"] is False
        assert "未找到知识" in result["error"]
        assert "nonexistent" in result["error"]


class TestKnowledgeFetchNoHost:
    """knowledge_fetch host=None 场景。"""

    def test_returns_failure_when_host_none(self):
        """host=None 时返回 success=False 且提示未加载。"""
        tool = KnowledgeFetchTool(host=None)
        result = tool.execute(name="any")

        assert result["success"] is False
        assert "未加载" in result["error"]


class TestKnowledgeSaveCreate:
    """knowledge_save 新建场景。"""

    def test_creates_project_knowledge_file(self, tmp_path):
        """项目级知识保存后，.knowledge/<name>.md 存在且 frontmatter 正确。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="express-route-order",
            level="project",
            description="express 路由顺序导致 404",
            content="# Express 路由\n正文内容",
            tags=["express", "bug"],
        )

        assert result["success"] is True
        assert result["name"] == "express-route-order"
        assert result["action"] == "created"

        saved = tmp_path / ".knowledge" / "experiences" / "express-route-order.md"
        assert saved.exists()
        raw = saved.read_text(encoding="utf-8")
        assert raw.startswith("---")
        assert "name: express-route-order" in raw
        assert "description: express 路由顺序导致 404" in raw
        assert "level: project" in raw
        assert "tags: [express, bug]" in raw
        assert "# Express 路由" in raw


class TestKnowledgeSaveOverwrite:
    """knowledge_save 覆盖场景。"""

    def test_overwrites_existing_file(self, tmp_path):
        """同名二次保存返回 action=updated 且内容被覆盖。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        tool.execute(
            name="react-hooks",
            level="project",
            description="旧描述",
            content="旧正文",
        )
        result = tool.execute(
            name="react-hooks",
            level="project",
            description="新描述",
            content="新正文",
        )

        assert result["success"] is True
        assert result["action"] == "updated"
        saved = tmp_path / ".knowledge" / "experiences" / "react-hooks.md"
        raw = saved.read_text(encoding="utf-8")
        assert "新正文" in raw
        assert "旧正文" not in raw


class TestKnowledgeSaveLevelPath:
    """knowledge_save level 决定路径。"""

    def test_global_level_writes_to_home(self, tmp_path, monkeypatch):
        """level=global 写到 ~/.abyss/knowledge/，用 monkeypatch 重定向 home。"""
        monkeypatch.setattr(Path, "home", lambda: tmp_path)
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="react-hooks",
            level="global",
            description="react hooks 闭包",
            content="正文",
        )

        assert result["success"] is True
        saved = tmp_path / ".abyss" / "knowledge" / "experiences" / "react-hooks.md"
        assert saved.exists()

    def test_project_level_writes_to_working_dir(self, tmp_path):
        """level=project 写到 <working_dir>/.knowledge/。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="react-hooks",
            level="project",
            description="react hooks",
            content="正文",
        )

        assert result["success"] is True
        saved = tmp_path / ".knowledge" / "experiences" / "react-hooks.md"
        assert saved.exists()
        # global 路径不应存在
        assert not (tmp_path / ".abyss").exists()


class TestKnowledgeSaveFailure:
    """knowledge_save 写入失败不抛异常。"""

    def test_returns_failure_without_raising(self, tmp_path, monkeypatch):
        """写入抛异常时，工具返回 success=False 不向上抛。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))

        def _raise(*args, **kwargs):
            raise OSError("permission denied")

        # 拦截 mkdir + write_text，模拟写入失败
        monkeypatch.setattr(Path, "mkdir", _raise)
        result = tool.execute(
            name="react-hooks",
            level="project",
            description="d",
            content="c",
        )

        assert result["success"] is False
        assert "error" in result


class TestKnowledgeSaveCategory:
    """knowledge_save category 参数写入对应子目录。"""

    def test_save_preference_to_subdir(self, tmp_path):
        """category=preference 写到 preferences/ 子目录。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="user-likes-type-hints",
            level="project",
            description="用户喜欢 type hints",
            content="正文",
            category="preference",
        )

        assert result["success"] is True
        saved = tmp_path / ".knowledge" / "preferences" / "user-likes-type-hints.md"
        assert saved.exists()

    def test_save_bugfix_to_subdir(self, tmp_path):
        """category=bugfix 写到 bugfixes/ 子目录。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="spinner-overwrite",
            level="project",
            description="spinner 覆盖 bug",
            content="正文",
            category="bugfix",
        )

        assert result["success"] is True
        saved = tmp_path / ".knowledge" / "bugfixes" / "spinner-overwrite.md"
        assert saved.exists()

    def test_save_experience_to_subdir(self, tmp_path):
        """category=experience 写到 experiences/ 子目录。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="react-patterns",
            level="project",
            description="React 模式",
            content="正文",
            category="experience",
        )

        assert result["success"] is True
        saved = tmp_path / ".knowledge" / "experiences" / "react-patterns.md"
        assert saved.exists()

    def test_save_tool_to_subdir(self, tmp_path, monkeypatch):
        """category=tool 写到 tools/ 子目录。"""
        monkeypatch.setattr(Path, "home", lambda: tmp_path)
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="file-write-usage",
            level="global",
            description="file_write 工具用法",
            content="正文",
            category="tool",
        )

        assert result["success"] is True
        saved = tmp_path / ".abyss" / "knowledge" / "tools" / "file-write-usage.md"
        assert saved.exists()

    def test_default_category_is_experience(self, tmp_path):
        """不传 category 时默认 experience，写到 experiences/ 子目录。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="some-knowledge",
            level="project",
            description="某知识",
            content="正文",
        )

        assert result["success"] is True
        saved = tmp_path / ".knowledge" / "experiences" / "some-knowledge.md"
        assert saved.exists()

    def test_category_in_frontmatter(self, tmp_path):
        """保存的文件 frontmatter 包含 category 字段。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        tool.execute(
            name="pref-1",
            level="project",
            description="偏好",
            content="正文",
            category="preference",
            tags=["python"],
        )

        saved = tmp_path / ".knowledge" / "preferences" / "pref-1.md"
        raw = saved.read_text(encoding="utf-8")
        assert "category: preference" in raw


class TestKnowledgeSaveBugfixLevelValidation:
    """bugfix 类型只允许 project 级。"""

    def test_bugfix_global_rejected(self, tmp_path, monkeypatch):
        """category=bugfix + level=global 应被拒绝。"""
        monkeypatch.setattr(Path, "home", lambda: tmp_path)
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="some-bug",
            level="global",
            description="bug",
            content="正文",
            category="bugfix",
        )

        assert result["success"] is False
        assert "bugfix" in result["error"].lower() or "project" in result["error"].lower()

    def test_bugfix_project_allowed(self, tmp_path):
        """category=bugfix + level=project 允许保存。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        result = tool.execute(
            name="some-bug",
            level="project",
            description="bug",
            content="正文",
            category="bugfix",
        )

        assert result["success"] is True


class TestKnowledgeSavePreferenceDedup:
    """preference 类型去重：语义相似时合并更新而非创建新条目。"""

    def test_duplicate_preference_merges_not_creates(self, tmp_path):
        """描述高度相似的偏好应合并到已有条目。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        # 先保存一个偏好
        tool.execute(
            name="user-prefers-type-hints",
            level="project",
            description="用户喜欢用 python type hints 做类型标注",
            content="偏好内容",
            category="preference",
            tags=["python", "typing"],
        )
        # 再保存一个高度相似的偏好（不同 name，但描述关键词高度重叠）
        result = tool.execute(
            name="user-likes-type-annotations",
            level="project",
            description="用户喜欢 python type hints 类型标注方式",
            content="更新后的偏好内容",
            category="preference",
            tags=["python", "typing"],
        )

        assert result["success"] is True
        # 应合并到已有条目，action=updated
        assert result["action"] == "updated"
        # 不应创建新文件
        new_file = tmp_path / ".knowledge" / "preferences" / "user-likes-type-annotations.md"
        assert not new_file.exists()
        # 原文件应被更新
        old_file = tmp_path / ".knowledge" / "preferences" / "user-prefers-type-hints.md"
        raw = old_file.read_text(encoding="utf-8")
        assert "更新后的偏好内容" in raw

    def test_non_duplicate_preference_creates_new(self, tmp_path):
        """描述不相似的偏好应创建新条目。"""
        tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        tool.execute(
            name="user-prefers-dark-theme",
            level="project",
            description="用户喜欢暗色主题界面",
            content="偏好A",
            category="preference",
            tags=["ui", "theme"],
        )
        result = tool.execute(
            name="user-prefers-type-hints",
            level="project",
            description="用户喜欢用 python type hints",
            content="偏好B",
            category="preference",
            tags=["python", "typing"],
        )

        assert result["success"] is True
        assert result["action"] == "created"
