"""Layer 3:LangGraphMind 作为 CognitiveMind 的 drop-in 替身(假模型,无网络)。"""

import pytest

from genesis.llm.client import ScriptedLLM
from genesis.runtime.brain_schema import (
    ActionDecisionOut, ActionSpec, BootstrapOut, DialogueDecisionOut, ReflectOut,
    RelationDelta, SpeechSpec,
)
from genesis.runtime.cognition import CognitiveMind, ThinkInput
from genesis.runtime import langgraph_mind as lgm


class _FakeStructured:
    def __init__(self, obj):
        self._obj = obj

    def invoke(self, messages):
        if self._obj is _BOOM:
            raise RuntimeError("结构化不支持")
        return self._obj


_BOOM = object()


class _FakeModel:
    """按请求的 schema 返回对应的预置对象(action vs dialogue)。"""

    def __init__(self, action_obj, dialogue_obj):
        self._action, self._dialogue = action_obj, dialogue_obj

    def with_structured_output(self, schema):
        return _FakeStructured(self._action if schema is ActionDecisionOut else self._dialogue)


def _make_mind(monkeypatch, action_obj, dialogue_obj):
    fake = _FakeModel(action_obj, dialogue_obj)
    monkeypatch.setattr(lgm, "build_chat_model", lambda **kw: fake)
    mind = lgm.LangGraphMind("江离", "你是编剧。", is_killer=False, home="门厅",
                             llm=ScriptedLLM([]), memory=None, static_ground="地图常识")
    mind.goals = [{"desc": "查清真相", "status": "todo", "note": ""}]   # 跳过 bootstrap 的 LLM 调用
    return mind


def _snap(**kw):
    base = dict(now_h=22.0, time_label="22:00", place="门厅", place_desc="宽敞的门厅。",
                occupants=[], events=[], soma_lines=[], candidates=[], bulletin="公告",
                memories=[], intent="保命")
    base.update(kw)
    return ThinkInput(**base)


def test_is_cognitivemind_subclass():
    assert issubclass(lgm.LangGraphMind, CognitiveMind)   # 引擎的 isinstance 分支照常命中


def test_action_decide_through_graph(monkeypatch):
    action = ActionDecisionOut(intent="赶往灯塔", thought="得快",
                               action=ActionSpec(kind="move", arg="灯塔"),
                               speech=SpeechSpec(to="", text="我去灯塔看看"))
    mind = _make_mind(monkeypatch, action, None)
    dec = mind.decide(_snap())
    assert dec.action_kind == "move" and dec.action_arg == "灯塔"
    assert dec.speech == "我去灯塔看看"
    assert dec.intent == "赶往灯塔"
    assert not dec.empty


def test_dialogue_decide_through_graph(monkeypatch):
    dlg = DialogueDecisionOut(thought="他在套话", speech="我先走了", leave=True)
    mind = _make_mind(monkeypatch, None, dlg)
    dec = mind.decide(_snap(dialogue_with="顾长风", dialogue=["顾长风:你去哪了?"]))
    assert dec.speech == "我先走了" and dec.speech_to == "顾长风"
    assert dec.leave_dialogue is True


def test_falls_back_to_text_parse_on_structured_failure(monkeypatch):
    # 结构化抛错 → 回落父类 _run_action(self._call → self.llm.complete 文本)
    mind = _make_mind(monkeypatch, _BOOM, None)
    # 让父类兜底路径的 ScriptedLLM 返回一段合法 JSON 文本
    mind.llm = ScriptedLLM(['{"thought":"兜底","intent":"观望","action":{"kind":"idle","arg":""}}'])
    dec = mind.decide(_snap())
    assert dec.action_kind == "idle"
    assert dec.thought == "兜底"


class _SchemaModel:
    """按 schema 返回对象;schema 映射到 _BOOM 表示该 schema 调用抛错。"""

    def __init__(self, by_schema):
        self._by = by_schema

    def with_structured_output(self, schema):
        return _FakeStructured(self._by.get(schema))


def _mind_by_schema(monkeypatch, by_schema):
    monkeypatch.setattr(lgm, "build_chat_model", lambda **kw: _SchemaModel(by_schema))
    return lgm.LangGraphMind("江离", "你是编剧。", is_killer=True, home="门厅",
                             llm=ScriptedLLM([]), memory=None, static_ground="地图常识")


def test_bootstrap_goals_through_graph(monkeypatch):
    mind = _mind_by_schema(monkeypatch, {BootstrapOut: BootstrapOut(
        goals=["去码头查船缆", "搜船屋", "回灯塔写手稿", "盯住顾长风", "多余的第5条"])})
    mind._bootstrap_goals()
    descs = [g["desc"] for g in mind.goals]
    assert descs == ["去码头查船缆", "搜船屋", "回灯塔写手稿", "盯住顾长风"]   # 截断到4条
    assert all(g["status"] == "todo" for g in mind.goals)


def test_bootstrap_fallback_on_failure(monkeypatch):
    mind = _mind_by_schema(monkeypatch, {BootstrapOut: _BOOM})
    mind.llm = ScriptedLLM(['{"goals": ["兜底目标A", "兜底目标B"]}'])
    mind._bootstrap_goals()
    assert [g["desc"] for g in mind.goals] == ["兜底目标A", "兜底目标B"]


def test_reflect_through_graph_applies_insights_and_trust(monkeypatch):

    class _Mem:
        def retrieve(self, *a, **k): return []
        def observe(self, *a, **k): pass

    out = ReflectOut(insights=["顾长风在隐瞒", "邵铭轩动机可疑"],
                     relations=[RelationDelta(name="顾长风", impression="闪烁其辞", trust=-0.4)])
    mind = _mind_by_schema(monkeypatch, {ReflectOut: out})
    mind.memory = _Mem()
    mind.reflect("23:30", longterm=None)
    assert list(mind.insights)[-2:] == ["顾长风在隐瞒", "邵铭轩动机可疑"]
    assert mind.relations["顾长风"]["trust"] == pytest.approx(-0.4)
    assert mind.relations["顾长风"]["impression"] == "闪烁其辞"


def test_latency_recorded(monkeypatch):
    from genesis.runtime.cognition import _LATENCY
    before = len(_LATENCY)
    action = ActionDecisionOut(action=ActionSpec(kind="idle"))
    mind = _make_mind(monkeypatch, action, None)
    mind.decide(_snap())
    assert len(_LATENCY) > before                        # /api/latency 仍有数据
    assert _LATENCY[-1][1] == "江离" and _LATENCY[-1][2] == "L2"
