"""用户 skill: 多来源覆盖(user wins)、加载失败收集、save_skill / fork_skill。"""
from __future__ import annotations

import tempfile
import unittest
from pathlib import Path

from core.skills import SkillRegistry, SkillSource
from tools.skill_authoring import ForkSkillTool, SaveSkillTool


def _write_skill(root: Path, name: str, desc: str, body: str = "body") -> Path:
    d = root / name
    d.mkdir(parents=True, exist_ok=True)
    (d / "SKILL.md").write_text(
        f"---\nname: {name}\ndescription: {desc}\n---\n\n# {name}\n\n{body}\n",
        encoding="utf-8",
    )
    return d


class TestMultiSourceRegistry(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.root = Path(self.tmp.name)
        self.builtin = self.root / "builtin"
        self.user = self.root / "user"
        self.builtin.mkdir()
        self.user.mkdir()

    def tearDown(self):
        self.tmp.cleanup()

    def _registry(self):
        return SkillRegistry([
            SkillSource(self.builtin, "builtin"),
            SkillSource(self.user, "user"),
        ])

    def test_user_overrides_builtin(self):
        _write_skill(self.builtin, "ppt", "内置 ppt")
        _write_skill(self.user, "ppt", "我的 ppt")
        reg = self._registry()
        self.assertEqual(reg.get("ppt").source, "user")
        self.assertEqual(reg.get("ppt").description, "我的 ppt")
        self.assertIn("ppt", reg.user_overrides)

    def test_distinct_names_coexist(self):
        _write_skill(self.builtin, "ppt", "内置 ppt")
        _write_skill(self.user, "ppt-mine", "我的 ppt")
        reg = self._registry()
        self.assertEqual(reg.get("ppt").source, "builtin")
        self.assertEqual(reg.get("ppt-mine").source, "user")
        self.assertNotIn("ppt-mine", reg.user_overrides)

    def test_discovery_block_tags_user_skills(self):
        _write_skill(self.builtin, "coding", "内置 coding")
        _write_skill(self.user, "ppt", "我的")  # 不撞内置
        _write_skill(self.builtin, "ppt", "内置 ppt")
        reg = self._registry()
        block = reg.discovery_block()
        self.assertIn("[你的·已覆盖内置]", block)  # user ppt 覆盖 builtin ppt

    def test_bad_user_skill_collected_not_crash(self):
        _write_skill(self.builtin, "coding", "内置")
        # 用户 skill: 缺 description
        bad = self.user / "broken"
        bad.mkdir()
        (bad / "SKILL.md").write_text("---\nname: broken\n---\nbody", encoding="utf-8")
        reg = self._registry()
        self.assertIn("coding", reg.skills)        # 没崩,内置正常
        self.assertNotIn("broken", reg.skills)     # 坏的没收
        self.assertTrue(any(n == "broken" for n, _ in reg.load_errors))
        self.assertIn("未加载", reg.discovery_block())

    def test_bad_yaml_user_skill_collected(self):
        bad = self.user / "badyaml"
        bad.mkdir()
        (bad / "SKILL.md").write_text("---\nname: [unclosed\n---\nbody", encoding="utf-8")
        reg = self._registry()
        self.assertTrue(any(n == "badyaml" for n, _ in reg.load_errors))

    def test_missing_user_dir_is_noop(self):
        _write_skill(self.builtin, "coding", "内置")
        reg = SkillRegistry([
            SkillSource(self.builtin, "builtin"),
            SkillSource(self.root / "does-not-exist", "user"),
        ])
        self.assertIn("coding", reg.skills)
        self.assertEqual(reg.load_errors, [])


class TestSaveSkillTool(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.root = Path(self.tmp.name)
        self.user_skills = self.root / ".skills"
        self.builtin = self.root / "builtin"
        self.builtin.mkdir()

    def tearDown(self):
        self.tmp.cleanup()

    def _tool(self):
        reg = SkillRegistry([
            SkillSource(self.builtin, "builtin"),
            SkillSource(self.user_skills, "user"),
        ])
        return SaveSkillTool(self.user_skills, reg)

    def test_save_writes_file(self):
        out = self._tool().execute(
            name="mine",
            content="---\nname: mine\ndescription: 我的 skill\n---\n\n# Mine\n",
        )
        self.assertIn("saved", out)
        self.assertTrue((self.user_skills / "mine" / "SKILL.md").exists())

    def test_reject_bad_name(self):
        out = self._tool().execute(name="../evil", content="---\ndescription: x\n---\n")
        self.assertIn("[Error]", out)
        self.assertFalse((self.user_skills / "../evil").exists())

    def test_reject_missing_description(self):
        out = self._tool().execute(name="mine", content="---\nname: mine\n---\nbody")
        self.assertIn("[Error]", out)
        self.assertIn("description", out)

    def test_reject_bad_yaml(self):
        out = self._tool().execute(name="mine", content="---\nname: [bad\n---\nbody")
        self.assertIn("[Error]", out)

    def test_reject_name_mismatch(self):
        out = self._tool().execute(
            name="mine",
            content="---\nname: other\ndescription: x\n---\n",
        )
        self.assertIn("[Error]", out)
        self.assertIn("不一致", out)

    def test_warns_on_builtin_override(self):
        _write_skill(self.builtin, "ppt", "内置 ppt")
        out = self._tool().execute(
            name="ppt",
            content="---\nname: ppt\ndescription: 我的 ppt\n---\n",
        )
        self.assertIn("saved", out)
        self.assertIn("覆盖内置", out)


class TestForkSkillTool(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.root = Path(self.tmp.name)
        self.user_skills = self.root / ".skills"
        self.builtin = self.root / "builtin"
        self.builtin.mkdir()
        # 带脚本的内置 skill
        d = _write_skill(self.builtin, "ppt", "内置 ppt")
        (d / "scripts").mkdir()
        (d / "scripts" / "helper.py").write_text("X = 1\n", encoding="utf-8")

    def tearDown(self):
        self.tmp.cleanup()

    def _tool(self):
        reg = SkillRegistry([
            SkillSource(self.builtin, "builtin"),
            SkillSource(self.user_skills, "user"),
        ])
        return ForkSkillTool(self.user_skills, reg)

    def test_fork_copies_scripts_and_renames(self):
        out = self._tool().execute(src="ppt", new_name="ppt-mine")
        self.assertIn("forked", out)
        dest = self.user_skills / "ppt-mine"
        self.assertTrue((dest / "scripts" / "helper.py").exists())  # 脚本带过来
        md = (dest / "SKILL.md").read_text(encoding="utf-8")
        self.assertIn("name: ppt-mine", md)       # frontmatter name 对齐新名
        self.assertNotIn("name: ppt\n", md)

    def test_forked_renamed_skill_does_not_shadow_builtin(self):
        self._tool().execute(src="ppt", new_name="ppt-mine")
        reg = SkillRegistry([
            SkillSource(self.builtin, "builtin"),
            SkillSource(self.user_skills, "user"),
        ])
        # 改了名,内置 ppt 不被遮,新名独立存在
        self.assertEqual(reg.get("ppt").source, "builtin")
        self.assertEqual(reg.get("ppt-mine").source, "user")

    def test_reject_existing_dest(self):
        self._tool().execute(src="ppt", new_name="ppt-mine")
        out = self._tool().execute(src="ppt", new_name="ppt-mine")
        self.assertIn("[Error]", out)
        self.assertIn("已存在", out)

    def test_reject_unknown_src(self):
        out = self._tool().execute(src="nope", new_name="x")
        self.assertIn("[Error]", out)
        self.assertIn("不存在", out)


if __name__ == "__main__":
    unittest.main()
