# -*- coding: utf-8 -*-
"""
LifecycleHost 知识库集成测试
覆盖：pre_llm 索引注入格式、去重、无命中不注入、get_knowledge 命中/未命中、
      knowledge 为 None 时不报错。
"""
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from abyss.knowledge import KnowledgeLoader
from abyss.runtime.host import LifecycleHost
from abyss.skills import SkillLoader


class FakeSession:
    """测试用 session 桩件，仅记录 append_to_system 累积文本。"""

    def __init__(self):
        self.system_content = ""

    def append_to_system(self, text: str) -> None:
        """模拟 Session.append_to_system，把文本累加到 system_content。"""
        self.system_content += text


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


def _make_host(knowledge_loader: KnowledgeLoader) -> LifecycleHost:
    """构造测试用 LifecycleHost：空 SkillLoader + mock mcp/hooks + 指定 knowledge。"""
    return LifecycleHost(
        skills=SkillLoader([]),
        mcp=MagicMock(),
        hooks=MagicMock(),
        working_dir="/tmp/fake",
        knowledge=knowledge_loader,
    )


class TestPreLlmIndexInjection:
    """pre_llm 索引注入测试。"""

    def test_global_index_injected_on_match(self, tmp_path):
        """关键词命中全局知识时，system 消息含 [G] 索引行。"""
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(
            global_dir / "react.md",
            "react-hooks-stale-closure",
            "react hooks 闭包陷阱的根因与修复方案",
        )
        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        host = _make_host(loader)

        session = FakeSession()
        host.pre_llm(session, "react hooks 闭包问题怎么解决")

        assert "[G][exp] react-hooks-stale-closure:" in session.system_content
        assert "相关知识索引" in session.system_content

    def test_project_index_injected_on_match(self, tmp_path):
        """关键词命中项目级知识时，system 消息含 [P] 索引行。"""
        project_dir = tmp_path / "project"
        project_dir.mkdir()
        _write_md(
            project_dir / "express.md",
            "express-route-order",
            "express 路由顺序导致 404 的坑",
        )
        loader = KnowledgeLoader({"global": None, "project": project_dir})
        loader.discover()
        host = _make_host(loader)

        session = FakeSession()
        host.pre_llm(session, "express 路由 404")

        assert "[P][exp] express-route-order:" in session.system_content

    def test_no_injection_when_no_keyword_match(self, tmp_path):
        """用户输入无关词时，system 消息不含「相关知识索引」段。"""
        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()
        host = _make_host(loader)

        session = FakeSession()
        host.pre_llm(session, "今天天气真好")

        assert "相关知识索引" not in session.system_content
        assert session.system_content == ""

    def test_no_duplicate_injection_on_second_pre_llm(self, tmp_path):
        """同一知识在两次 pre_llm 中重复命中，第二次不重复注入索引。"""
        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()
        host = _make_host(loader)

        session1 = FakeSession()
        host.pre_llm(session1, "react hooks 闭包问题")

        session2 = FakeSession()
        host.pre_llm(session2, "react hooks 闭包问题")

        # 第二次 pre_llm 不应再注入索引（去重）
        assert "相关知识索引" not in session2.system_content
        assert session2.system_content == ""
        # 第一次只注入一次索引行
        assert session1.system_content.count("[G][exp] react-hooks:") == 1


class TestGetKnowledge:
    """get_knowledge 方法测试。"""

    def test_get_knowledge_hit(self, tmp_path):
        """按名获取已加载知识，返回 Knowledge 对象。"""
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        _write_md(
            global_dir / "react.md",
            "react-hooks",
            "react hooks 闭包",
            body="详细正文内容",
        )
        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        host = _make_host(loader)

        k = host.get_knowledge("react-hooks")
        assert k is not None
        assert k.name == "react-hooks"
        assert k.content == "详细正文内容"

    def test_get_knowledge_miss(self, tmp_path):
        """按名获取不存在知识，返回 None。"""
        global_dir = tmp_path / "global"
        global_dir.mkdir()
        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        host = _make_host(loader)

        assert host.get_knowledge("nonexistent") is None


class TestKnowledgeNone:
    """knowledge 为 None 时不报错测试。"""

    def test_get_knowledge_returns_none_when_knowledge_none(self):
        """host 持有 knowledge=None 时，get_knowledge 返回 None。"""
        host = LifecycleHost(
            skills=SkillLoader([]),
            mcp=MagicMock(),
            hooks=MagicMock(),
            working_dir="/tmp/fake",
            knowledge=None,
        )
        assert host.get_knowledge("any") is None

    def test_pre_llm_no_error_when_knowledge_none(self):
        """host 持有 knowledge=None 时，pre_llm 不报错且不注入。"""
        host = LifecycleHost(
            skills=SkillLoader([]),
            mcp=MagicMock(),
            hooks=MagicMock(),
            working_dir="/tmp/fake",
            knowledge=None,
        )
        session = FakeSession()
        host.pre_llm(session, "任意输入")
        assert session.system_content == ""


class TestStartTriggersDiscover:
    """host.start() 触发 knowledge.discover() 测试。"""

    def test_start_calls_knowledge_discover(self, tmp_path):
        """host.start() 应调用 knowledge.discover()，否则磁盘文件不会被加载。"""
        # 在全局目录下准备一个知识文件
        global_dir = tmp_path / "global"
        cat_dir = global_dir / "tools"
        cat_dir.mkdir(parents=True)
        _write_md(cat_dir / "my-tool.md", "my-tool", "工具经验")

        loader = KnowledgeLoader({
            "global": global_dir,
            "project": tmp_path / "project",
        })
        host = _make_host(loader)

        # start 之前 loader 应为空
        assert loader.list_all() == []

        host.start()

        # start 之后应已加载磁盘文件
        all_k = loader.list_all()
        assert len(all_k) == 1
        assert all_k[0].name == "my-tool"


class TestPreferenceAlwaysInjected:
    """preference 类知识在第一次 pre_llm 时无条件注入（不依赖关键词匹配）。

    用户名字、偏好、常用语言等基础画像信息必须常驻 system prompt，
    这样用户问"我是谁/我喜欢什么"时 Agent 能立刻回答。
    """

    def test_preference_injected_on_unrelated_input(self, tmp_path):
        """用户输入无关词时，preference 类知识仍应被注入。"""
        global_dir = tmp_path / "global"
        pref_dir = global_dir / "preferences"
        pref_dir.mkdir(parents=True)
        _write_md(
            pref_dir / "user-name.md",
            "user-name",
            "用户名字是力克木",
            category="preference",
            tags=["name", "likeemu"],
        )
        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        host = _make_host(loader)

        session = FakeSession()
        # 用户输入完全不相关
        host.pre_llm(session, "今天天气真好")

        assert "user-name" in session.system_content
        assert "力克木" in session.system_content

    def test_preference_not_re_injected_on_second_pre_llm(self, tmp_path):
        """第二次 pre_llm 时 preference 不重复注入（常驻一次即可）。"""
        global_dir = tmp_path / "global"
        pref_dir = global_dir / "preferences"
        pref_dir.mkdir(parents=True)
        _write_md(
            pref_dir / "user-name.md",
            "user-name",
            "用户名字是力克木",
            category="preference",
        )
        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        host = _make_host(loader)

        session1 = FakeSession()
        host.pre_llm(session1, "你好")
        session2 = FakeSession()
        host.pre_llm(session2, "你好")

        # 第二次不应再注入 preference
        assert "user-name" not in session2.system_content
        assert session2.system_content == ""

    def test_multiple_preferences_all_injected(self, tmp_path):
        """多条 preference 类知识都应在第一次 pre_llm 时被注入。"""
        global_dir = tmp_path / "global"
        pref_dir = global_dir / "preferences"
        pref_dir.mkdir(parents=True)
        _write_md(
            pref_dir / "user-name.md",
            "user-name",
            "用户名字是力克木",
            category="preference",
        )
        _write_md(
            pref_dir / "user-lang.md",
            "user-lang",
            "用户偏好 Python 语言",
            category="preference",
        )
        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        host = _make_host(loader)

        session = FakeSession()
        host.pre_llm(session, "随便说点什么")

        assert "user-name" in session.system_content
        assert "user-lang" in session.system_content

    def test_experience_still_requires_keyword_match(self, tmp_path):
        """experience 类知识仍走关键词匹配，无关输入不注入。"""
        global_dir = tmp_path / "global"
        exp_dir = global_dir / "experiences"
        exp_dir.mkdir(parents=True)
        _write_md(
            exp_dir / "react-hooks.md",
            "react-hooks",
            "react hooks 闭包陷阱",
            category="experience",
        )
        loader = KnowledgeLoader({"global": global_dir, "project": None})
        loader.discover()
        host = _make_host(loader)

        session = FakeSession()
        host.pre_llm(session, "今天天气真好")

        # experience 类不应被注入（无关键词命中）
        assert "react-hooks" not in session.system_content
