"""仿真线场景测试:整夜全链路体检(凶手三幕/夜话两场/传播/终局/通道送达矩阵)。

这里的断言都是『舞台机械』不变量——心智的具体决策由剧本脑给出,真实局里由 LLM 自由发挥。
跑过过去三个真实局踩过的全部坑:kill 不可达、幻想凶案、对话锁死、夜话无人表态。
"""

import asyncio

import pytest

from genesis.runtime.engine import NIGHT_COURT_H
from tests.simline import run_night


@pytest.fixture(scope="module")
def night():
    """整夜只跑一次(~数秒),所有断言共享这份轨迹。"""
    return asyncio.run(run_night())


# ───────────────── 主线节拍:三幕落地,钟点有效 ─────────────────

def test_first_act_lands_within_script_window(night):
    eng, brain, ms = night
    assert ms["kills"], f"整夜零凶案——凶手链路断裂。主线:{[n['text'] for n in eng.narrative]}"
    first = ms["kills"][0][1]
    # 七天版:首案落在第一夜(开局 21:00 之后、第一场夜话与天亮快进之前的窗口内)。
    # 上界放宽到夜话回笼+凌晨;只要第一夜内有案即合格(逐日复仇,不再绑单一钟点)。
    assert 21.0 <= first <= 29.6, f"第一案时间失控:{first:.2f}h(应落在第一夜)"


def test_multiple_acts_and_kill_chain_artifacts(night):
    eng, brain, ms = night
    assert len(ms["kills"]) >= 2, f"整夜仅 {len(ms['kills'])} 案,节拍器未持续推进"
    # 凶案装置链:胶片盒按幕烧毁 + 残页钉在尸旁
    acts = int(eng.state.facilities["film_boxes"].state.replace("act", ""))
    organic = [d for d in eng.state.dead if d not in eng.executed]
    assert acts == min(len(organic), 7), "胶片盒倒计时与实际凶案数不符"
    for victim in organic:
        page = eng.state.items.get(f"pinned_{victim}")
        assert page is not None and not page.hidden, f"{victim} 尸旁无剧本残页"
    assert "白聿" not in eng.state.dead, "凶手对白聿下不去手——人性裂缝被破坏"


def test_killer_never_books_a_kill_without_receipt(night):
    eng, brain, ms = night
    killer = eng.minds[eng.killer_name]
    for g in killer.goals or []:
        if g["status"] == "done" and any(w in g["desc"] for w in ("kill", "杀", "清算")):
            named = [n for n in eng.minds if n != eng.killer_name and n in g["desc"]]
            assert all(n in eng.state.dead for n in named), f"无回执的 done:{g}"


# ───────────────── 夜话:两场都开,投票通道畅通 ─────────────────

def test_night_courts_fire_on_schedule(night):
    eng, brain, ms = night
    hours = [h for _, h in ms["courts"]]
    assert any(abs(h - NIGHT_COURT_H[0]) < 0.5 for h in hours), f"23:00 夜话未触发:{hours}"


def test_first_court_collects_votes_not_silence(night):
    eng, brain, ms = night
    # 过去真实局的事故:全员锁在会话里,『无人表态』流产。现在投票期强拆会话+错峰唤醒
    assert ms["accusations"], "夜话零表态——投票通道仍然不通"
    assert any("指认" in a for a in ms["accusations"]), f"无人指认:{ms['accusations'][:4]}"
    silent = [n["text"] for n in eng.narrative if "无人表态" in n["text"]]
    assert not silent, "又出现『无人表态』流产"


def test_finale_justice_via_court(night):
    eng, brain, ms = night
    assert ms["ending"] is not None, f"渡船将至仍无终局(end_hour={ms['end_hour']:.1f})"
    assert ms["ending"]["kind"] == "凶手伏法", f"剧本脑合力指认下应伏法,实际:{ms['ending']}"
    assert eng.killer_name in ms["executed"]


# ───────────────── 信息与通道:送达矩阵 ─────────────────

def test_kill_scream_reaches_far_survivors_memory(night):
    eng, brain, ms = night
    survivor = next(a for a in eng.minds if a not in eng.state.dead and a != eng.killer_name)
    hits = eng.minds[survivor].memory.retrieve("惨叫 出事", 30.0, top_k=8)
    assert any("惨叫" in h.content for h in hits), f"{survivor} 整夜没听见任何惨叫广播"


def test_killer_pressure_reaches_both_think_modes(night):
    eng, brain, ms = night
    kn = eng.killer_name
    l2 = [u for (n, k, s, u) in brain.prompts if n == kn and k == "l2"]
    assert any("世界回执" in u for u in l2), "凶手 L2 没收到世界回执"
    dlg = [u for (n, k, s, u) in brain.prompts if n == kn and k == "dialogue"]
    if dlg:   # 程亦深的黏人人设几乎必然把凶手拖进过会话
        assert any("世界回执" in u for u in dlg), "凶手在对话模式收不到回执(对话锁死bug回归)"


def test_vote_phase_prompts_reach_multiple_attendees(night):
    eng, brain, ms = night
    voters = {n for (n, k, s, u) in brain.prompts if k == "l2" and "对质投票正在进行" in u}
    assert len(voters) >= 3, f"投票期只有 {voters} 收到表态提示——强制唤醒失效"


def test_common_ground_in_every_decision_prompt(night):
    eng, brain, ms = night
    for n, k, s, u in brain.prompts:
        if k in ("l2", "dialogue", "bootstrap"):
            assert "雾鸦岛地图常识" in s, f"{n} 的 {k} prompt 缺地图共识"


# ───────────────── 节奏总账:一夜跑完,时长可控 ─────────────────

def test_night_completes_and_report(night):
    eng, brain, ms = night
    assert ms["end_hour"] <= 29.6, "拖过渡船时刻仍未收束"
    lines = [f"凶案 {t:.2f}h @{p}" for (_, t, p) in ms["kills"]]
    print("\n═══ 仿真线整夜体检报告 ═══")
    print(f"  幕:{len(ms['kills'])} 起 — {lines}")
    print(f"  夜话:{[f'{h:.2f}h' for _, h in ms['courts']]} | 表态 {len(ms['accusations'])} 次")
    print(f"  终局:{ms['ending']['kind']} @ {ms['end_hour']:.2f}h | 处决:{ms['executed']}")
    print(f"  prompt 留底:{len(brain.prompts)} 份")
