# -*- coding: utf-8 -*-
"""
回归测试：会话恢复时的消息序列修复。

Bug 复现：用户按 Esc 中断任务后保存会话，最后一条 assistant 消息可能带
tool_calls 但缺少对应的 tool 角色响应。恢复会话时这些不完整历史发给 API，
触发 400 错误：
  "An assistant message with 'tool_calls' must be followed by tool messages
   responding to each 'tool_call_id'"

修复：Session.from_file() 加载时主动清洗历史，截断尾部不完整的 tool_calls 序列。
"""
import json
from pathlib import Path

import pytest

from abyss.session import Session


SYSTEM = "你是助手"


def _write_session(tmp_path: Path, messages: list, name: str = "latest") -> Path:
    """写一个最小化的会话 JSON 文件。"""
    p = tmp_path / f"{name}.json"
    p.write_text(json.dumps({
        "messages": messages,
        "tool_call_counter": 99,
    }, ensure_ascii=False), encoding="utf-8")
    return p


def _tc(idx: str, name: str = "todo_write"):
    """构造一个 tool_call dict。"""
    return {
        "id": idx,
        "type": "function",
        "function": {"name": name, "arguments": "{}"},
    }


# ---------- 合法序列：不应被截断 ----------

def test_load_valid_sequence_unchanged(tmp_path):
    """合法序列：assistant(tool_calls) 后跟齐 tool messages，应原样加载。"""
    msgs = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": "", "tool_calls": [_tc("c1")]},
        {"role": "tool", "tool_call_id": "c1", "content": "result1"},
    ]
    p = _write_session(tmp_path, msgs)

    s = Session.from_file(p, SYSTEM)

    assert [m["role"] for m in s.messages] == ["system", "user", "assistant", "tool"]


def test_load_multi_tool_calls_all_matched(tmp_path):
    """一次 assistant 发 3 个 tool_calls，后面 3 条 tool 消息覆盖，合法。"""
    msgs = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": "", "tool_calls": [_tc("c1"), _tc("c2"), _tc("c3")]},
        {"role": "tool", "tool_call_id": "c1", "content": "r1"},
        {"role": "tool", "tool_call_id": "c2", "content": "r2"},
        {"role": "tool", "tool_call_id": "c3", "content": "r3"},
    ]
    p = _write_session(tmp_path, msgs)

    s = Session.from_file(p, SYSTEM)

    assert len(s.messages) == 6


# ---------- Bug 复现：尾部缺 tool messages ----------

def test_load_trailing_unmatched_tool_calls_truncated(tmp_path):
    """Bug：尾部 assistant(tool_calls) 后无 tool 响应，应截断 assistant 及之后消息。"""
    msgs = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": "正在处理", "tool_calls": [_tc("c1")]},
        # 缺少 tool c1 的响应
        {"role": "user", "content": "继续吧"},
    ]
    p = _write_session(tmp_path, msgs)

    s = Session.from_file(p, SYSTEM)

    # assistant(带 tool_calls) 及其后的 user 都应被截断
    assert [m["role"] for m in s.messages] == ["system", "user"]
    # 截断的 user "继续吧" 应保留下来（作为最新用户输入继续对话）
    # 不，安全起见全截断：避免上下文出现悬挂的 user 但缺少对应 assistant
    # 这里只验证截断到不完整 assistant 之前


def test_load_partial_tool_responses_truncated(tmp_path):
    """3 个 tool_calls 只跟了 2 个 tool 响应，应截断 assistant 及之后所有。"""
    msgs = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": "", "tool_calls": [_tc("c1"), _tc("c2"), _tc("c3")]},
        {"role": "tool", "tool_call_id": "c1", "content": "r1"},
        {"role": "tool", "tool_call_id": "c2", "content": "r2"},
        # 缺 c3 的响应
        {"role": "user", "content": "继续"},
    ]
    p = _write_session(tmp_path, msgs)

    s = Session.from_file(p, SYSTEM)

    # 截断到 user("hi") 为止，去掉所有未配对的内容
    assert [m["role"] for m in s.messages] == ["system", "user"]


def test_load_trailing_assistant_without_tool_calls_kept(tmp_path):
    """尾部 assistant 不带 tool_calls 时应保留，不需修复。"""
    msgs = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": "你好"},
    ]
    p = _write_session(tmp_path, msgs)

    s = Session.from_file(p, SYSTEM)

    assert len(s.messages) == 3


# ---------- 中间不完整也应截断（防御性） ----------

def test_load_middle_unmatched_truncated_from_that_point(tmp_path):
    """中间某条 assistant(tool_calls) 缺响应，从该点截断后续所有消息。"""
    msgs = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "first"},
        {"role": "assistant", "content": "", "tool_calls": [_tc("c1")]},
        # 缺 c1 响应，但后面又出现了新的对话
        {"role": "user", "content": "second"},
        {"role": "assistant", "content": "reply"},
    ]
    p = _write_session(tmp_path, msgs)

    s = Session.from_file(p, SYSTEM)

    # 从不完整的 assistant 处截断，保留 system + user("first")
    assert [m["role"] for m in s.messages] == ["system", "user"]


# ---------- cwd 恢复：恢复会话时切换到原工作目录 ----------

def test_session_save_restore_cwd(tmp_path):
    """Session 保存时应记录 cwd，恢复时应切回原目录。"""
    import os
    original_cwd = os.getcwd()
    try:
        # 模拟在 tmp_path 目录下创建会话
        os.chdir(str(tmp_path))
        session = Session(SYSTEM)
        session.add_user("hi")
        session.add_assistant(content="hello")
        assert session.cwd == str(tmp_path)

        # 保存
        save_path = tmp_path / "test_session.json"
        session.save(save_path)

        # 切到别的目录模拟用户重启 abyss
        os.chdir(str(tmp_path.parent))

        # 恢复会话：应切回 tmp_path
        restored = Session.from_file(save_path, SYSTEM)
        assert restored.cwd == str(tmp_path)
        assert os.getcwd() == str(tmp_path)
    finally:
        os.chdir(original_cwd)


def test_session_restore_missing_cwd_fallback(tmp_path):
    """旧版会话文件没有 cwd 字段时，应静默降级到当前 CWD。"""
    import os
    # 写一个不带 cwd 的旧格式会话文件
    p = tmp_path / "old_session.json"
    p.write_text(json.dumps({
        "messages": [{"role": "system", "content": SYSTEM}],
        "tool_call_counter": 0,
        "started_at": 1234567890,
    }, ensure_ascii=False), encoding="utf-8")

    s = Session.from_file(p, SYSTEM)
    # cwd 应为当前目录（降级）
    assert s.cwd == os.getcwd()


def test_session_restore_nonexistent_cwd_fallback(tmp_path):
    """会话记录的 cwd 目录已不存在时，应静默降级到当前 CWD。"""
    import os
    original_cwd = os.getcwd()
    try:
        # 在 tmp_path 下创建会话并保存
        os.chdir(str(tmp_path))
        session = Session(SYSTEM)
        save_path = tmp_path / "test.json"
        session.save(save_path)

        # 切到别处
        os.chdir(str(tmp_path.parent))
        # 删除原目录
        import shutil
        shutil.rmtree(str(tmp_path))

        # 恢复会话：目录不存在，应降级
        restored = Session.from_file(save_path, SYSTEM)
        assert restored.cwd == os.getcwd()  # 当前 CWD
    finally:
        os.chdir(original_cwd)


# ---------- 文件不存在 / 损坏 ----------

def test_load_missing_file_returns_new_session(tmp_path):
    p = tmp_path / "nope.json"

    s = Session.from_file(p, SYSTEM)

    assert len(s.messages) == 1
    assert s.messages[0]["role"] == "system"


def test_load_corrupt_file_returns_new_session(tmp_path):
    p = tmp_path / "bad.json"
    p.write_text("not json at all", encoding="utf-8")

    s = Session.from_file(p, SYSTEM)

    assert len(s.messages) == 1


# ---------- tool_call_id 顺序无关 ----------

def test_load_tool_responses_out_of_order_still_valid(tmp_path):
    """tool 消息顺序与 tool_calls 顺序不一致也应接受（API 只看 id 匹配）。"""
    msgs = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": "", "tool_calls": [_tc("c1"), _tc("c2")]},
        {"role": "tool", "tool_call_id": "c2", "content": "r2"},
        {"role": "tool", "tool_call_id": "c1", "content": "r1"},
    ]
    p = _write_session(tmp_path, msgs)

    s = Session.from_file(p, SYSTEM)

    assert len(s.messages) == 5  # 完整保留


# ---------- 实际 Bug 场景复现 ----------

def test_load_real_bug_scenario(tmp_path):
    """实际 latest.json 的 bug 场景：
    最后一条 assistant 调了 todo_write 但 tool 响应丢失，后接 user 消息。
    """
    msgs = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "帮我做任务"},
        {"role": "assistant", "content": "", "tool_calls": [_tc("call_00", "todo_write")]},
        # 缺 todo_write 的执行结果（被 Esc 中断）
        {"role": "user", "content": "在src目录下的核心文件"},
    ]
    p = _write_session(tmp_path, msgs)

    s = Session.from_file(p, SYSTEM)

    # 应截断到不完整 assistant 之前
    assert [m["role"] for m in s.messages] == ["system", "user"]
    # 验证 user 内容是"帮我做任务"，而非被截断的"在src目录下的核心文件"
    assert s.messages[1]["content"] == "帮我做任务"
