"""持久化单测(G20):snapshot → 同参新引擎 restore → 状态/记忆/剧情无损,且能继续运行。"""

import asyncio

from genesis.runtime.engine import WorldEngine
from genesis.runtime.persistence import (
    load_from_file,
    restore_engine,
    save_to_file,
    snapshot_engine,
)


class FakeTime:
    def __init__(self):
        self.t = 0.0

    def __call__(self):
        return self.t


class FakeLLM:
    """脚本桩(同 test_cognition):固定回复,零真实调用。"""

    def __init__(self, default: str = '{"thought":"先去书房看看","intent":"查个究竟",'
                                      '"speech":null,"action":{"kind":"move","arg":"书房"},"minutes":5}'):
        self.default = default
        self.calls: list[list[dict]] = []

    def complete(self, messages, **kw):
        self.calls.append(messages)
        return self.default


async def _drive(eng, iters: int, ft, step_real: float = 0.5, sleep_s: float = 0.05):
    task = asyncio.create_task(eng.sched.run(eng.on_wake))
    for _ in range(iters):
        ft.t += step_real
        await asyncio.sleep(sleep_s)
    eng.sched.stop()
    await asyncio.wait_for(task, timeout=2)


def _stub_engine(ft) -> WorldEngine:
    return WorldEngine("worlds/ravenisle", rate=600.0, time_fn=ft, demo_beats=False)


def _mutate_world(eng: WorldEngine) -> None:
    """人为制造一批可变状态:设施变更/取物/杀人/叙事/灯油,覆盖快照的各个分支。"""
    eng.state.set_facility("switchboard", "smashed")
    eng.state.power_on = False
    eng.state.weather = "雷暴"
    it = eng.state.items["draft"]                  # 书房的报道原稿:翻出 → 拿走
    it.hidden = False
    eng.state.positions["温言"] = "书房"
    assert eng.state.take_item("draft", "温言") is not None
    eng.state.positions["程亦深"] = "琴房"          # 杀一人(与 _do_kill 的客观处置一致)
    eng.state.kill("程亦深", "琴房")
    eng.sched.drop("程亦深")
    eng.bus.unregister("程亦深")
    eng.minds["程亦深"].actor.current = None
    eng._narr("💥 全岛骤然停电,黑暗里人声四起。")
    eng._narr("☠ 程亦深 在琴房遇害,无人目击。")
    eng.lighthouse_fuel = 2


# ───────────────── 桩模式:世界状态全量往返 ─────────────────

def test_snapshot_restore_stub_world():
    async def main():
        ft = FakeTime()
        eng = _stub_engine(ft)
        eng.start()
        await _drive(eng, iters=30, ft=ft)        # 跑 ~2.5 模拟小时,产生移动/睡眠等状态
        return eng

    eng = asyncio.run(main())
    _mutate_world(eng)
    data = snapshot_engine(eng)

    eng2 = _stub_engine(FakeTime())
    restore_engine(eng2, data)

    # 时钟:模拟时刻与倍率断点续走
    assert abs(eng2.clock.sim_seconds - eng.clock.sim_seconds) < 1e-6
    assert eng2.base_rate == eng.base_rate
    # 世界客观事实逐项一致
    assert eng2.state.positions == eng.state.positions
    assert eng2.state.dead == {"程亦深": "琴房"}
    assert eng2.state.bodies["琴房"] == ["程亦深"]
    assert eng2.state.weather == "雷暴" and eng2.state.power_on is False
    assert eng2.state.facilities["switchboard"].state == "smashed"
    assert eng2.state.items["draft"].holder == "温言"
    assert not eng2.state.items["draft"].hidden
    assert eng2.lighthouse_fuel == 2
    assert [n["text"] for n in eng2.narrative] == [n["text"] for n in eng.narrative]
    # 心智:soma 与最近言行(StubMind 无 intent/memory,getattr 容错路径)
    for name, m1 in eng.minds.items():
        m2 = eng2.minds[name]
        assert m2.last_speech == m1.last_speech
        for f in ("last_meal_h", "woke_at_h", "fear", "_fear_at_h"):
            assert getattr(m2.soma, f) == getattr(m1.soma, f), (name, f)
        assert m2.gathering == m1.gathering
    # 死者已退出总线(不再收事件)
    assert "程亦深" not in eng2.bus._inboxes


def test_restored_engine_keeps_running():
    """恢复后 start() 能正常续跑:活人继续行动,死人不复活。"""
    async def main():
        ft = FakeTime()
        eng = _stub_engine(ft)
        eng.start()
        await _drive(eng, iters=20, ft=ft)
        _mutate_world(eng)
        data = snapshot_engine(eng)

        ft2 = FakeTime()
        eng2 = _stub_engine(ft2)
        restore_engine(eng2, data)
        t0 = eng2.clock.sim_seconds
        eng2.start()
        await _drive(eng2, iters=20, ft=ft2)
        return eng2, t0

    eng2, t0 = asyncio.run(main())
    assert eng2.clock.sim_seconds > t0                       # 时间从断点继续
    assert "程亦深" in eng2.state.dead                        # 死人没有复活
    assert "程亦深" not in eng2.state.positions
    assert eng2.state.facilities["switchboard"].state == "smashed"   # 世界事实仍持久
    assert eng2.frame(1)["killer"] == "江离"                  # viz 帧正常可出


# ───────────────── LLM 模式:意图与记忆无损 ─────────────────

def test_snapshot_restore_llm_minds_and_memory():
    async def main():
        ft = FakeTime()
        eng = WorldEngine("worlds/ravenisle", rate=600.0, time_fn=ft,
                          demo_beats=False, brain="llm", llm=FakeLLM())
        eng.start()
        await _drive(eng, iters=40, ft=ft, step_real=0.4)
        return eng

    eng = asyncio.run(main())
    # 制造一条高重要度目击记忆(覆盖 importance/created_at 的保真)
    eng.minds["温言"].memory.observe("第1天 23:00 深夜,我在琴房亲眼目睹了可怕的一幕!!",
                                     23.0, importance=1.0)
    data = snapshot_engine(eng)

    eng2 = WorldEngine("worlds/ravenisle", rate=600.0, time_fn=FakeTime(),
                       demo_beats=False, brain="llm", llm=FakeLLM())
    restore_engine(eng2, data)

    for name, m1 in eng.minds.items():
        m2 = eng2.minds[name]
        # 意图/言行/对话窗口
        assert m2.intent == m1.intent, name
        assert m2.last_speech == m1.last_speech
        assert m2.last_thought == m1.last_thought
        assert list(m2.dialogue) == list(m1.dialogue)
        assert m2.dialogue_with == m1.dialogue_with
        for f in ("last_meal_h", "woke_at_h", "fear", "_fear_at_h"):
            assert getattr(m2.soma, f) == getattr(m1.soma, f), (name, f)
        # 记忆:数量与逐字段内容完全一致
        r1 = m1.memory.store.all(name)
        r2 = m2.memory.store.all(name)
        assert len(r2) == len(r1) and len(r1) > 0, name
        for a, b in zip(r1, r2):
            assert (b.content, b.created_at, b.last_accessed, b.kind, b.importance,
                    b.agent_id, b.id) == \
                   (a.content, a.created_at, a.last_accessed, a.kind, a.importance,
                    a.agent_id, a.id)
    # 恢复后的记忆可正常检索,且新写入不与旧 id 冲突
    hits = eng2.minds["温言"].memory.retrieve("琴房 目睹", 23.5, top_k=3)
    assert any("亲眼目睹" in h.content for h in hits)
    new_rec = eng2.minds["温言"].memory.observe("恢复后的新记忆", 24.0)
    assert all(new_rec.id != r["id"] for r in data["minds"]["温言"]["memory"])


# ───────────────── 说书人前情 + 文件往返 ─────────────────

def test_save_load_file_roundtrip_with_narrator(tmp_path):
    from genesis.runtime.narrator import Narrator

    async def main():
        ft = FakeTime()
        eng = _stub_engine(ft)
        eng.narrator = Narrator(FakeLLM())
        eng.start()
        await _drive(eng, iters=15, ft=ft)
        return eng

    eng = asyncio.run(main())
    _mutate_world(eng)
    eng.narrator.paras.append({"t": "第1天 23:40 深夜", "text": "风暴撕扯着鸦堡的窗棂……"})
    eng.narrator.summary = "停电之夜,琴房传来噩耗。"
    eng.narrator.buffer.append("[琴房] 远处传来一声凄厉的惨叫")

    path = tmp_path / "saves" / "world.json"
    save_to_file(eng, path)
    assert path.exists()

    eng2 = _stub_engine(FakeTime())
    eng2.narrator = Narrator(FakeLLM())
    data = load_from_file(eng2, path)

    assert eng2.narrator.paras == eng.narrator.paras
    assert eng2.narrator.summary == "停电之夜,琴房传来噩耗。"
    assert eng2.narrator.buffer == eng.narrator.buffer
    # 快照的幂等性:恢复出来的世界再快照,与存档逐字段相等(JSON 往返无损)
    assert snapshot_engine(eng2) == data


def test_restore_without_narrator_is_tolerant(tmp_path):
    """存档带说书人、新引擎没注入 narrator 时,容错跳过而非报错。"""
    from genesis.runtime.narrator import Narrator

    eng = _stub_engine(FakeTime())
    eng.narrator = Narrator(FakeLLM())
    path = save_to_file(eng, tmp_path / "world.json")

    eng2 = _stub_engine(FakeTime())          # 未注入 narrator
    load_from_file(eng2, path)
    assert eng2.narrator is None             # 不炸,且不无中生有
