"""runtime 地基单测:时钟连续性、总线可见性三档、调度顺序与中断、躯体曲线。"""

import asyncio

from genesis.runtime.bus import Event, EventBus
from genesis.runtime.scheduler import SimScheduler
from genesis.runtime.simclock import SimClock
from genesis.runtime.soma import Soma, fatigue_curve, hunger_curve


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

    def __call__(self):
        return self.t


# ───────────────────────── SimClock ─────────────────────────

def test_clock_rate_and_continuity():
    ft = FakeTime()
    c = SimClock(rate=30, start_hour=21, time_fn=ft)
    ft.t += 60                       # 真实 1 分钟 → 模拟 30 分钟
    assert abs(c.hour - 21.5) < 1e-6
    c.set_rate(60)                   # 变速不跳变
    assert abs(c.hour - 21.5) < 1e-6
    ft.t += 60                       # 60× 下真实 1 分钟 → 模拟 1 小时
    assert abs(c.hour - 22.5) < 1e-6
    assert "22:30" in c.label()


def test_clock_pause():
    ft = FakeTime()
    c = SimClock(rate=30, start_hour=8, time_fn=ft)
    c.pause(); ft.t += 100
    assert abs(c.hour - 8.0) < 1e-6
    c.resume(); ft.t += 120          # 恢复后正常走
    assert abs(c.hour - 9.0) < 1e-6


# ───────────────────────── EventBus ─────────────────────────

def _world():
    pos = {"甲": "门厅", "乙": "门厅", "丙": "琴房", "丁": "灯塔"}
    adj = {"门厅": ["琴房"], "琴房": ["门厅"], "灯塔": []}
    return pos, adj


def test_bus_place_and_adjacent_muffled():
    pos, adj = _world()
    bus = EventBus(lambda p: [a for a, q in pos.items() if q == p], lambda p: adj.get(p, []))
    inbox: dict[str, list[Event]] = {a: [] for a in pos}
    for a in pos:
        bus.register(a, inbox[a].append)
    got = bus.publish(Event(etype="speech", content="有人尖叫", place="门厅", actor="甲",
                            salience=0.9, muffled="隔壁传来一声闷响"))
    assert "乙" in got and "甲" not in got            # 同地点收到,发起者不收
    assert inbox["乙"][0].content == "有人尖叫"
    assert inbox["丙"][0].content == "隔壁传来一声闷响"  # 相邻衰减版
    assert inbox["丙"][0].salience < 0.9
    assert inbox["丁"] == []                           # 不相邻,无感知


def test_bus_global_and_private():
    pos, adj = _world()
    bus = EventBus(lambda p: [a for a, q in pos.items() if q == p], lambda p: adj.get(p, []))
    inbox: dict[str, list[Event]] = {a: [] for a in pos}
    for a in pos:
        bus.register(a, inbox[a].append)
    bus.publish(Event(etype="ambient", content="全岛骤然停电", scope="global", actor="江"))
    assert all(len(inbox[a]) == 1 for a in pos)        # 全员收到(无 actor"江"注册)
    bus.publish(Event(etype="feedback", content="你砸毁了电闸", scope="private", to=["甲"]))
    assert inbox["甲"][-1].content == "你砸毁了电闸" and len(inbox["乙"]) == 1


# ───────────────────────── Scheduler ─────────────────────────

def test_scheduler_order_and_interrupt():
    async def main():
        ft = FakeTime()
        clock = SimClock(rate=60, start_hour=0, time_fn=ft)
        sched = SimScheduler(clock)
        fired: list[tuple[str, str]] = []

        async def handler(aid, reason):
            fired.append((aid, reason))
            if len(fired) >= 3:
                sched.stop()

        sched.schedule("乙", clock.sim_seconds + 120, "tick")     # 模拟 2 分钟后
        sched.schedule("甲", clock.sim_seconds + 60, "tick")      # 模拟 1 分钟后(先到)
        task = asyncio.create_task(sched.run(handler))
        await asyncio.sleep(0.05)
        ft.t += 1.0                       # 真实 1s → 模拟 60s:甲到点
        await asyncio.sleep(0.1)
        sched.interrupt("丙", "惨叫")      # 中断立即触发
        await asyncio.sleep(0.1)
        ft.t += 1.0                       # 乙到点
        await asyncio.wait_for(task, timeout=2)
        assert fired[0][0] == "甲" and fired[1] == ("丙", "interrupt:惨叫") and fired[2][0] == "乙"

    asyncio.run(main())


def test_scheduler_reschedule_replaces_old():
    async def main():
        ft = FakeTime()
        clock = SimClock(rate=60, start_hour=0, time_fn=ft)
        sched = SimScheduler(clock)
        fired = []

        async def handler(aid, reason):
            fired.append(reason)
            sched.stop()

        sched.schedule("甲", clock.sim_seconds + 60, "旧预约")
        sched.schedule("甲", clock.sim_seconds + 60, "新预约")   # 顶掉旧的
        task = asyncio.create_task(sched.run(handler))
        await asyncio.sleep(0.05)
        ft.t += 1.2
        await asyncio.wait_for(task, timeout=2)
        assert fired == ["新预约"]

    asyncio.run(main())


# ───────────────────────── Soma ─────────────────────────

def test_hunger_meal_anchored_not_linear():
    assert hunger_curve(1.0, 10.0) < 0.05          # 刚吃过不饿
    assert hunger_curve(4.0, 10.0) < 0.35          # 4 小时仅微饿
    assert hunger_curve(8.0, 16.0) > 0.85          # 8 小时真饿
    assert hunger_curve(4.0, 12.5) > hunger_curve(4.0, 10.0)  # 饭点窗口抬升


def test_fatigue_circadian():
    assert fatigue_curve(23.5, 16.0) > 0.5         # 深夜困
    assert fatigue_curve(15.0, 7.0) < 0.1          # 白天清醒
    assert fatigue_curve(15.0, 20.0) > 0.15        # 熬夜有睡眠债


def test_fear_suppresses_needs_and_decays():
    s = Soma(last_meal_h=12.0, woke_at_h=8.0)
    hungry = s.snapshot(23.0)
    assert hungry["hunger"] > 0.8 and hungry["fatigue"] > 0.4   # 深夜没吃饭:又饿又困
    s.shock(23.0, 1.0)                                           # 目击尸体
    scared = s.snapshot(23.0)
    assert scared["fear"] > 0.9
    assert scared["hunger"] < hungry["hunger"] * 0.5             # 肾上腺素压住食欲
    later = s.snapshot(23.0 + 2.0)                               # 两小时后恐惧大幅衰减
    assert later["fear"] < 0.15


def test_prompt_lines_thresholded():
    s = Soma(last_meal_h=19.0, woke_at_h=8.0)
    assert s.prompt_lines(21.0) == []              # 刚吃过晚饭、不困不怕:零躯体噪音
    assert any("吃" in c for c in Soma(last_meal_h=12.0).candidates(21.0))
