"""skill 定向模型(frontmatter `model:`)测试。

覆盖两层:
1. Skill.from_dir 解析 frontmatter 可选 `model` 字段(无 → "";有 → strip 后原样)。
2. AgentLoop._execute_tool_call 的热切挂点:load_skill 成功且 switcher 返回新
   (profile, caps, llm) → self.caps/self.llm 替换 + 结果尾追加说明 + emit model_switch;
   load_skill 报错 / switcher 抛异常 → 不切换、原模型继续。

switcher 本体(内置 skill 判定 + 档位豁免 + tasks 持久化)在 agent_builder 闭包里,
依赖 DB,不在此测;loop 侧只关心"给了切换指令就正确执行、失败不破坏 run"。
"""
from __future__ import annotations

import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from core.loop import AgentLoop  # noqa: E402
from core.skills import Skill  # noqa: E402


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


class TestSkillModelField(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.TemporaryDirectory()
        self.root = Path(self.tmpdir.name)

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

    def test_no_model_field_defaults_empty(self):
        sk = Skill.from_dir(_write_skill(self.root, "plain"))
        self.assertEqual(sk.model, "")

    def test_model_field_parsed_and_stripped(self):
        sk = Skill.from_dir(_write_skill(self.root, "pinned", "model:  glm.pro52 \n"))
        self.assertEqual(sk.model, "glm.pro52")


class _FakeSession:
    task_id = "00000000-0000-0000-0000-000000000000"
    messages: list = []

    def append(self, msg):
        return 1


class _FakeExecutor:
    """load_skill 返回可配置文本;其余不需要。"""

    def __init__(self, result_text: str):
        self._result = result_text

    def schemas(self):
        return []

    def call_tool(self, name, args, ctx):
        return SimpleNamespace(content=self._result)


def _fake_caps(family="deepseek_v4", variant="flash"):
    return SimpleNamespace(
        family=family, variant=variant, max_iterations=10,
        default_reasoning_effort="", reliable_context=64000,
    )


def _make_loop(result_text: str, switcher) -> AgentLoop:
    loop = AgentLoop(
        llm=SimpleNamespace(tag="old_llm"),
        executor=_FakeExecutor(result_text),
        session=_FakeSession(),
        capabilities=_fake_caps(),
        user_id="00000000-0000-0000-0000-000000000000",
        working_dir=Path("."),
        skill_model_switch=switcher,
    )
    loop.events = []
    loop.sink = SimpleNamespace(emit=lambda ev: loop.events.append(ev))
    return loop


def _load_skill_tc(skill_name="ppt"):
    return SimpleNamespace(
        id="tc1",
        function=SimpleNamespace(name="load_skill", arguments=f'{{"name": "{skill_name}"}}'),
    )


class TestLoopHotSwap(unittest.TestCase):
    def test_switch_applies_new_caps_and_llm(self):
        new_caps = _fake_caps("glm", "pro52")
        new_llm = SimpleNamespace(tag="new_llm")
        calls = []

        def switcher(skill_name, cur_profile):
            calls.append((skill_name, cur_profile))
            return "glm.pro52", new_caps, new_llm

        loop = _make_loop("[skill=ppt, dir=x]\n# PPT", switcher)
        result, _ = loop._execute_tool_call(_load_skill_tc())

        self.assertEqual(calls, [("ppt", "deepseek_v4.flash")])
        self.assertIs(loop.caps, new_caps)
        self.assertIs(loop.llm, new_llm)
        self.assertIn("[模型切换]", result)
        self.assertIn("glm.pro52", result)
        types = [e["type"] for e in loop.events]
        self.assertIn("model_switch", types)
        ms = next(e for e in loop.events if e["type"] == "model_switch")
        self.assertEqual(ms["model_profile"], "glm.pro52")
        self.assertEqual(ms["from"], "deepseek_v4.flash")

    def test_no_switch_when_switcher_returns_none(self):
        loop = _make_loop("[skill=ppt, dir=x]\n# PPT", lambda n, c: None)
        old_caps, old_llm = loop.caps, loop.llm
        result, _ = loop._execute_tool_call(_load_skill_tc())
        self.assertIs(loop.caps, old_caps)
        self.assertIs(loop.llm, old_llm)
        self.assertNotIn("[模型切换]", result)
        self.assertNotIn("model_switch", [e["type"] for e in loop.events])

    def test_error_result_skips_switcher(self):
        calls = []
        loop = _make_loop(
            "[Error] skill 'nope' not found.",
            lambda n, c: calls.append(n),
        )
        loop._execute_tool_call(_load_skill_tc("nope"))
        self.assertEqual(calls, [])

    def test_switcher_exception_warns_and_keeps_model(self):
        def switcher(n, c):
            raise FileNotFoundError("glm.yaml missing")

        loop = _make_loop("[skill=ppt, dir=x]\n# PPT", switcher)
        old_caps, old_llm = loop.caps, loop.llm
        result, _ = loop._execute_tool_call(_load_skill_tc())
        self.assertIs(loop.caps, old_caps)
        self.assertIs(loop.llm, old_llm)
        self.assertNotIn("[模型切换]", result)
        warns = [e for e in loop.events if e["type"] == "warn"]
        self.assertTrue(any("切换失败" in e["msg"] for e in warns))

    def test_other_tools_never_trigger_switcher(self):
        calls = []
        loop = _make_loop("file content", lambda n, c: calls.append(n))
        tc = SimpleNamespace(
            id="tc2", function=SimpleNamespace(name="read", arguments='{"path": "a.txt"}')
        )
        loop._execute_tool_call(tc)
        self.assertEqual(calls, [])


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