"""
专门测试：最新真实用户消息及之后的上下文保护机制。

验证要点：
1. 最新用户消息在压缩后始终存在
2. 用户消息之后的所有轮次（assistant/tool）不被压缩、不丢消息
3. 用户消息之前的轮次可正常压缩（丢弃 + 摘要）
4. 伪 user 消息（[System] 开头）不干扰真正用户消息的识别
5. 无用户消息时退化到旧版 behavior（protect_first + summary + keep_last）
6. protect_latest_user_round=False 时正常工作
7. session_manager 非破坏性：add_message 始终成功，current_messages 不丢数据
"""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest
from aacode.utils.message_utils import (
    estimate_tokens,
    split_into_rounds,
    build_compact_view,
    validate_tool_call_integrity,
    _find_latest_real_user_round,
    _find_last_n_real_user_round,
)


def _make_test_encoding():
    return None


def _sys(text="s"):
    return {"role": "system", "content": text}


def _user(text="q"):
    return {"role": "user", "content": text}


def _asst(text="a", tool_calls=None):
    return {"role": "assistant", "content": text, "tool_calls": tool_calls or []}


def _tool(call_id, text="r"):
    return {"role": "tool", "tool_call_id": call_id, "content": text}


def _pseudo_user(text="[System] Tool execution result below..."):
    """系统注入的伪 user 消息，lib 不应该把它当做真正的用户消息"""
    return {"role": "user", "content": text}


def _tc(name="f", args="{}", idx=0):
    return {"id": f"c{idx}", "type": "function", "function": {"name": name, "arguments": args}}


# ═══════════════════════════════════════════════════════════════
# _find_latest_real_user_round
# ═══════════════════════════════════════════════════════════════

class TestFindLatestRealUserRound:
    def test_normal_user_found(self):
        rounds = [[_sys()], [_user("task")], [_asst(tool_calls=[_tc()])], [_tool("c0")]]
        assert _find_latest_real_user_round(rounds) == 1

    def test_ignores_pseudo_user(self):
        rounds = [[_sys()], [_user("real")], [_asst()], [_pseudo_user()], [_asst()]]
        idx = _find_latest_real_user_round(rounds)
        assert idx == 1, f"Should find real user at idx 1, got {idx}"

    def test_pseudo_user_only_returns_none(self):
        rounds = [[_sys()], [_pseudo_user()], [_asst()]]
        assert _find_latest_real_user_round(rounds) is None

    def test_no_user_returns_none(self):
        rounds = [[_sys()], [_asst(tool_calls=[_tc()])], [_tool("c0")]]
        assert _find_latest_real_user_round(rounds) is None

    def test_multiple_real_users_returns_latest(self):
        rounds = [[_user("old")], [_asst()], [_user("new")], [_asst()]]
        assert _find_latest_real_user_round(rounds) == 2

    def test_ignores_tool_role(self):
        rounds = [[_sys()], [_tool("x")], [_user("real")]]
        assert _find_latest_real_user_round(rounds) == 2


# ═══════════════════════════════════════════════════════════════
# build_compact_view — 最新用户保护
# ═══════════════════════════════════════════════════════════════

class TestLatestUserProtection:
    """验证最新真实 user 消息及之后的轮次在压缩视图中的完整性"""

    def test_latest_user_always_present(self):
        """最新用户消息在压缩视图中始终存在"""
        msgs = [_sys(), _asst(tool_calls=[_tc(idx=0)]), _tool("c0"),
                _user("REAL_TASK"),
                _asst(tool_calls=[_tc(idx=1)]), _tool("c1")]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is True
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert "REAL_TASK" in user_texts

    def test_post_user_rounds_intact(self):
        """用户消息之后的 assistant/tool 轮次完整保留"""
        msgs = [
            _sys(),
            _asst("pre_a0", tool_calls=[_tc(idx=0)]), _tool("c0", "pre_r0"),
            _asst("pre_a1", tool_calls=[_tc(idx=1)]), _tool("c1", "pre_r1"),
            _user("TASK"),
            _asst("post_a2", tool_calls=[_tc(idx=2)]), _tool("c2", "post_r2"),
            _asst("post_a3", tool_calls=[_tc(idx=3)]), _tool("c3", "post_r3"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is True
        validate_tool_call_integrity(view)
        asst_texts = [m.get("content", "") for m in view if m.get("role") == "assistant"]
        assert any("post_a2" in t for t in asst_texts), "post-user assistant post_a2 missing"
        assert any("post_a3" in t for t in asst_texts), "post-user assistant post_a3 missing"
        tool_texts = [m.get("content", "") for m in view if m.get("role") == "tool"]
        assert "post_r2" in tool_texts, "post-user tool post_r2 missing"
        assert "post_r3" in tool_texts, "post-user tool post_r3 missing"

    def test_pre_user_rounds_compressed(self):
        """用户消息之前的轮次可以被压缩"""
        msgs = [
            _sys(),
            _asst("a0", tool_calls=[_tc(idx=0)]), _tool("c0", "r0"),
            _asst("a1", tool_calls=[_tc(idx=1)]), _tool("c1", "r1"),
            _asst("a2", tool_calls=[_tc(idx=2)]), _tool("c2", "r2"),
            _user("TASK"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is True
        # pre-user rounds 被压缩，r1/r2 的工具消息不应出现
        tool_contents = [m.get("content", "") for m in view if m.get("role") == "tool"]
        assert "r1" not in tool_contents, "pre-user tool r1 should be compressed away"
        assert "r2" not in tool_contents, "pre-user tool r2 should be compressed away"

    def test_pre_user_protect_first_respected(self):
        """protect_first_rounds 正确保护用户消息之前的 N 轮"""
        msgs = [
            _sys(),                                                                  # round 0
            _asst("a0", tool_calls=[_tc(idx=0)]), _tool("c0", "r0"),                # round 1 (keep)
            _asst("a1", tool_calls=[_tc(idx=1)]), _tool("c1", "r1"),                # round 2
            _asst("a2", tool_calls=[_tc(idx=2)]), _tool("c2", "r2"),                # round 3
            _user("TASK"),                                                            # round 4
            _asst("a3", tool_calls=[_tc(idx=3)]), _tool("c3", "r3"),                # round 5
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=2,  # keep sys + a0/r0
        )
        assert compacted is True
        validate_tool_call_integrity(view)
        tool_contents = [m.get("content", "") for m in view if m.get("role") == "tool"]
        # r0 should be kept (in first 2 rounds)
        assert "r0" in tool_contents, "pre-user r0 within protect_first should be kept"
        # r1, r2 should be compressed away
        assert "r1" not in tool_contents, "pre-user r1 beyond protect_first should be compressed"
        assert "r2" not in tool_contents, "pre-user r2 beyond protect_first should be compressed"

    def test_all_post_user_preserved_regardless_of_count(self):
        """无论用户消息之后有多少轮，全部保留"""
        msgs = [_sys()]
        # 5 轮 pre-user
        for i in range(5):
            msgs.append(_asst(tool_calls=[_tc(idx=i)]))
            msgs.append(_tool(f"c{i}", f"pre_r{i}"))
        msgs.append(_user("TASK"))
        # 20 轮 post-user
        for i in range(20):
            msgs.append(_asst(tool_calls=[_tc(idx=100 + i)]))
            msgs.append(_tool(f"c{100 + i}", f"post_r{i}"))
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is True
        validate_tool_call_integrity(view)
        # 所有 post-user 的工具消息必须存在
        post_view_tools = [m.get("content", "") for m in view if m.get("role") == "tool"
                           and "post_r" in str(m.get("content", ""))]
        assert len(post_view_tools) == 20, f"Expected 20 post-user tool messages, got {len(post_view_tools)}"

    def test_user_is_first_message_no_compression(self):
        """当 user 消息是第一条（没有 pre-user）时，不做压缩"""
        msgs = [_user("TASK"), _asst(tool_calls=[_tc()]), _tool("c0")]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is False
        assert len(view) == len(msgs)

    def test_user_after_system_only_no_compression(self):
        """只有 system 在 user 之前时，没有要压缩的内容"""
        msgs = [_sys(), _user("TASK"), _asst(tool_calls=[_tc()]), _tool("c0")]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        # pre_user=[sys], protect_first=1, skipped=0 → no compaction
        assert len(view) == len(msgs)


# ═══════════════════════════════════════════════════════════════
# pseudo-user 消息防护
# ═══════════════════════════════════════════════════════════════

class TestPseudoUserHandling:
    """伪 user 消息（[System] 开头）不应干扰真正用户的保护"""

    def test_pseudo_user_does_not_block_real_user_compression(self):
        """有伪 user 消息时，真正的用户消息仍然驱动压缩逻辑"""
        msgs = [
            _sys(),
            _asst(tool_calls=[_tc(idx=0)]), _tool("c0"),  # pre-user
            _asst(tool_calls=[_tc(idx=1)]), _tool("c1"),  # pre-user
            _pseudo_user(),                                # 系统伪 user
            _asst(tool_calls=[_tc(idx=2)]), _tool("c2"),  # 伪 user 之后的轮次
            _user("REAL_TASK"),                            # 真正的用户消息
            _asst(tool_calls=[_tc(idx=3)]), _tool("c3"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is True
        validate_tool_call_integrity(view)
        # REAL_TASK 及其之后的轮次必须保留
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert "REAL_TASK" in user_texts
        assert "[System]" not in [str(m.get("content", "")) for m in view if m["role"] == "user" if "REAL_TASK" not in str(m.get("content", ""))]

    def test_only_pseudo_user_no_real_compacts_fallback(self):
        """只有伪 user 消息时，退化到旧版 fallback 逻辑"""
        msgs = [
            _sys(),
            _pseudo_user(),
            _asst(tool_calls=[_tc(idx=0)]), _tool("c0"),
            _asst(tool_calls=[_tc(idx=1)]), _tool("c1"),
            _asst(tool_calls=[_tc(idx=2)]), _tool("c2"),
            _asst(tool_calls=[_tc(idx=3)]), _tool("c3"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1, keep_last_rounds=1,
        )
        assert compacted is True
        validate_tool_call_integrity(view)


# ═══════════════════════════════════════════════════════════════
# protect_latest_user_round=False
# ═══════════════════════════════════════════════════════════════

class TestProtectLatestUserRoundDisabled:
    """protect_latest_user_round=False 时的退化行为"""

    def test_disabled_falls_back_to_old_behavior(self):
        msgs = [
            _sys(),
            _asst(tool_calls=[_tc(idx=0)]), _tool("c0"),
            _asst(tool_calls=[_tc(idx=1)]), _tool("c1"),
            _user("TASK"),
            _asst(tool_calls=[_tc(idx=2)]), _tool("c2"),
            _asst(tool_calls=[_tc(idx=3)]), _tool("c3"),
            _asst(tool_calls=[_tc(idx=4)]), _tool("c4"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1, keep_last_rounds=2,
            protect_latest_user_round=False,
        )
        assert compacted is True
        validate_tool_call_integrity(view)
        # 旧逻辑：keep_last_rounds=2 只保留最后 2 轮，用户消息可能被丢弃
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        # 用户消息不在最后 2 轮，会被丢弃
        assert "TASK" not in user_texts, (
            "With protect_latest_user_round=False, TASK should be in middle and dropped"
        )

    def test_disabled_short_circuit(self):
        """protect_latest_user_round=False 且轮次不足时短路"""
        msgs = [_sys(), _user("t"), _asst(), _tool("c0")]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=2, keep_last_rounds=2,
            protect_latest_user_round=False,
        )
        # 4 rounds <= 2+2=4 → 短路不压缩
        assert compacted is False
        assert len(view) == len(msgs)


# ═══════════════════════════════════════════════════════════════
# edge cases
# ═══════════════════════════════════════════════════════════════

class TestEdgeCases:
    """边界情况"""

    def test_empty_messages(self):
        view, compacted, _ = build_compact_view(_make_test_encoding(), [], max_tokens=100)
        assert compacted is False
        assert view == []

    def test_under_budget_no_compaction(self):
        msgs = [_sys(), _user("t"), _asst(tool_calls=[_tc()]), _tool("c0")]
        view, compacted, _ = build_compact_view(_make_test_encoding(), msgs, max_tokens=1000000)
        assert compacted is False
        assert len(view) == len(msgs)

    def test_tool_call_integrity_preserved_across_compaction(self):
        """压缩后 tool_calls/tool_messages 配对不破坏"""
        msgs = [
            _sys(),
            _asst("pre_a", tool_calls=[_tc(idx=0), _tc(idx=1)]),
            _tool("c0", "pre_r0"),
            _tool("c1", "pre_r1"),
            _user("TASK"),
            _asst("post_a", tool_calls=[_tc(idx=2), _tc(idx=3)]),
            _tool("c2", "post_r2"),
            _tool("c3", "post_r3"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is True
        validate_tool_call_integrity(view)

    def test_original_messages_unchanged(self):
        msgs = [_sys(), _asst(tool_calls=[_tc(idx=0)]), _tool("c0"), _user("TASK")]
        original_len = len(msgs)
        build_compact_view(_make_test_encoding(), msgs, max_tokens=1, protect_first_rounds=1)
        assert len(msgs) == original_len
        assert msgs[3]["content"] == "TASK"

    def test_summary_inserted_between_pre_and_post_user(self):
        """摘要消息插入在 pre-user 和 post-user 之间"""
        msgs = [
            _sys(),
            _asst(tool_calls=[_tc(idx=0)]), _tool("c0"),
            _asst(tool_calls=[_tc(idx=1)]), _tool("c1"),
            _user("TASK"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is True
        # 找到 system message 的摘要
        sys_msgs = [m for m in view if m.get("role") == "system" and "Context Summary" in str(m.get("content", ""))]
        assert len(sys_msgs) == 1
        # 摘要应在 user 消息之前
        summary_idx = view.index(sys_msgs[0])
        user_idx = next(i for i, m in enumerate(view) if m.get("role") == "user")
        assert summary_idx < user_idx, f"Summary at {summary_idx} should be before user at {user_idx}"

    def test_multiple_identical_messages(self):
        """多条相同内容的消息不应互相干扰"""
        msgs = [_sys(), _user("same")]
        for _ in range(3):
            msgs.append(_asst(tool_calls=[_tc()]))
            msgs.append(_tool("c0"))
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        validate_tool_call_integrity(view)


# ═══════════════════════════════════════════════════════════════
# 模拟真实多轮会话场景
# ═══════════════════════════════════════════════════════════════

class TestRealWorldScenarios:
    """模拟真实多轮会话场景"""

    def test_multi_task_session(self):
        """多任务会话：多个 user 消息，压缩保护最后 N 条用户消息（N=2）"""
        msgs = [
            _sys(),
            # Task 1
            _user("Task 1: build feature X"),
            _asst("planning X", tool_calls=[_tc(idx=10)]),
            _tool("c10", "plan result"),
            _asst("implementing X", tool_calls=[_tc(idx=11)]),
            _tool("c11", "code result"),
            _asst("Task 1 done"),
            # Task 2
            _user("Task 2: fix bug Y"),
            _asst("analyzing Y", tool_calls=[_tc(idx=20)]),
            _tool("c20", "analysis result"),
            _asst("fixing Y", tool_calls=[_tc(idx=21)]),
            _tool("c21", "fix result"),
            # Task 3
            _user("Task 3: refactor Z"),
            _asst("refactoring Z", tool_calls=[_tc(idx=30)]),
            _tool("c30", "refactor result"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        assert compacted is True
        validate_tool_call_integrity(view)
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        # 最后 2 条用户消息（Task 2, Task 3）及其附带消息完整保护
        assert any("Task 2" in t for t in user_texts), "Task 2 should be preserved (last 2 user rounds)"
        assert any("Task 3" in t for t in user_texts), "Task 3 should be preserved (latest user round)"

    def test_consecutive_user_rounds(self):
        """连续多条 user 消息（用户快速追问）"""
        msgs = [
            _sys(),
            _asst(tool_calls=[_tc(idx=0)]), _tool("c0"),
            _user("question 1"),
            _user("question 2"),
            _user("question 3"),  # latest
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
        )
        validate_tool_call_integrity(view)
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert any("question 3" in t for t in user_texts), "Latest user question should be preserved"

    def test_large_session_with_many_rounds(self):
        """大量轮次模拟"""
        msgs = [_sys()]
        # 30 轮 pre-user history
        for i in range(30):
            msgs.append(_asst(f"pre_a{i}", tool_calls=[_tc(idx=i)]))
            msgs.append(_tool(f"c{i}", f"pre_r{i}"))
        # user
        msgs.append(_user("CURRENT_TASK"))
        # 50 轮 post-user
        for i in range(50):
            msgs.append(_asst(f"post_a{i}", tool_calls=[_tc(idx=100 + i)]))
            msgs.append(_tool(f"c{100 + i}", f"post_r{i}"))
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=3,
        )
        assert compacted is True
        validate_tool_call_integrity(view)
        # 最新 user 存在
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert "CURRENT_TASK" in user_texts
        # 所有 50 轮 post-user 完整保留
        post_view_tools = [m.get("content", "") for m in view
                           if m.get("role") == "tool" and "post_r" in str(m.get("content", ""))]
        assert len(post_view_tools) == 50, f"Expected 50 post-user tool msgs, got {len(post_view_tools)}"


# ═══════════════════════════════════════════════════════════════
# _find_last_n_real_user_round
# ═══════════════════════════════════════════════════════════════

class TestFindLastNRealUserRound:
    def test_n1_returns_latest(self):
        """n=1 等价于 _find_latest_real_user_round"""
        rounds = [[_sys()], [_user("task1")], [_asst()], [_user("task2")], [_asst()]]
        assert _find_last_n_real_user_round(rounds, 1) == 3

    def test_n2_returns_second_to_last(self):
        """n=2 返回倒数第 2 条用户消息所在 round"""
        rounds = [[_sys()], [_user("task1")], [_asst()], [_user("task2")], [_asst()]]
        assert _find_last_n_real_user_round(rounds, 2) == 1

    def test_n_exceeds_count_returns_earliest(self):
        """不足 n 条时返回最早用户消息的索引"""
        rounds = [[_sys()], [_user("task1")], [_asst()]]
        assert _find_last_n_real_user_round(rounds, 3) == 1

    def test_no_real_user_returns_none(self):
        """无真实用户消息时返回 None"""
        rounds = [[_sys()], [_pseudo_user()], [_asst()]]
        assert _find_last_n_real_user_round(rounds, 2) is None

    def test_ignores_pseudo_user(self):
        """跳过伪 user 消息"""
        rounds = [[_sys()], [_pseudo_user()], [_user("real")], [_asst()]]
        assert _find_last_n_real_user_round(rounds, 2) == 2

    def test_default_n_is_2(self):
        """默认 n=2"""
        rounds = [[_sys()], [_user("first")], [_asst()], [_user("second")], [_asst()]]
        assert _find_last_n_real_user_round(rounds) == 1

    def test_single_user_with_n2(self):
        """只有 1 条用户消息，n=2 返回该条"""
        rounds = [[_sys()], [_user("only")], [_asst()]]
        assert _find_last_n_real_user_round(rounds, 2) == 1


# ═══════════════════════════════════════════════════════════════
# build_compact_view — protect_last_user_rounds 参数
# ═══════════════════════════════════════════════════════════════

class TestProtectLastNUserRounds:
    """验证 protect_last_user_rounds 参数行为"""

    def test_n2_both_users_protected(self):
        """protect_last_user_rounds=2: 最后 2 条用户消息及附带消息全部保留"""
        msgs = [
            _sys(),
            _asst("pre_a", tool_calls=[_tc(idx=0)]), _tool("c0", "pre_r"),
            _user("Task A"),
            _asst("a_a", tool_calls=[_tc(idx=1)]), _tool("c1", "r_a1"),
            _asst("a_a2", tool_calls=[_tc(idx=2)]), _tool("c2", "r_a2"),
            _user("Task B"),
            _asst("a_b", tool_calls=[_tc(idx=3)]), _tool("c3", "r_b"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
            protect_last_user_rounds=2,
        )
        assert compacted is True
        validate_tool_call_integrity(view)
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert "Task A" in user_texts, "Task A should be preserved (last 2 user rounds)"
        assert "Task B" in user_texts, "Task B should be preserved (last 2 user rounds)"

    def test_n2_task_a_tools_preserved(self):
        """保护边界从 Task A 开始，Task A 的 tool 消息也完整保留"""
        msgs = [
            _sys(),
            _asst("pre_a", tool_calls=[_tc(idx=0)]), _tool("c0", "pre_r"),
            _user("Task A"),
            _asst("a_a", tool_calls=[_tc(idx=1)]), _tool("c1", "r_a1"),
            _user("Task B"),
            _asst("a_b", tool_calls=[_tc(idx=2)]), _tool("c2", "r_b"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
            protect_last_user_rounds=2,
        )
        validate_tool_call_integrity(view)
        tool_contents = [m.get("content", "") for m in view if m.get("role") == "tool"]
        assert "r_a1" in tool_contents, "Task A tool results should be preserved"
        assert "r_b" in tool_contents, "Task B tool results should be preserved"
        assert "pre_r" not in tool_contents, "Pre-user tools should be compressed"

    def test_n1_equivalent_to_old_behavior(self):
        """protect_last_user_rounds=1: 等价于旧版行为（只保护最近 1 条）"""
        msgs = [
            _sys(),
            _asst("pre_a", tool_calls=[_tc(idx=0)]), _tool("c0", "pre_r"),
            _user("Task A"),
            _asst("a_a", tool_calls=[_tc(idx=1)]), _tool("c1", "r_a1"),
            _user("Task B"),
            _asst("a_b", tool_calls=[_tc(idx=2)]), _tool("c2", "r_b"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
            protect_last_user_rounds=1,
        )
        validate_tool_call_integrity(view)
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert "Task B" in user_texts, "Latest user must be protected"
        assert "Task A" not in user_texts, "With n=1, only latest is protected"

    def test_n2_single_user_round(self):
        """只有 1 条用户消息时，n=2 保护该条及之后全部"""
        msgs = [
            _sys(),
            _asst("pre_a", tool_calls=[_tc(idx=0)]), _tool("c0", "pre_r"),
            _asst("pre_a2", tool_calls=[_tc(idx=1)]), _tool("c1", "pre_r2"),
            _user("ONLY_TASK"),
            _asst("post_a", tool_calls=[_tc(idx=2)]), _tool("c2", "post_r"),
            _asst("post_a2", tool_calls=[_tc(idx=3)]), _tool("c3", "post_r2"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
            protect_last_user_rounds=2,
        )
        validate_tool_call_integrity(view)
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert "ONLY_TASK" in user_texts
        # 所有 post-user 工具消息必须保留
        tool_contents = [m.get("content", "") for m in view if m.get("role") == "tool"]
        assert "post_r" in tool_contents
        assert "post_r2" in tool_contents

    def test_n3_all_three_protected(self):
        """3 条用户消息，n=3 保护全部，pre_user 可能为空"""
        msgs = [
            _sys(),
            _user("Task 1"),
            _asst("a1", tool_calls=[_tc(idx=0)]), _tool("c0", "r0"),
            _user("Task 2"),
            _asst("a2", tool_calls=[_tc(idx=1)]), _tool("c1", "r1"),
            _user("Task 3"),
            _asst("a3", tool_calls=[_tc(idx=2)]), _tool("c2", "r2"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=1,
            protect_last_user_rounds=3,
        )
        validate_tool_call_integrity(view)
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert "Task 1" in user_texts
        assert "Task 2" in user_texts
        assert "Task 3" in user_texts

    def test_protect_first_rounds_reduced(self):
        """protect_first_rounds=2 时，早期轮次适度保留，最新 2 条用户消息完整保护"""
        msgs = [
            _sys(),
            _asst("a0", tool_calls=[_tc(idx=0)]), _tool("c0", "r0"),
            _asst("a1", tool_calls=[_tc(idx=1)]), _tool("c1", "r1"),
            _asst("a2", tool_calls=[_tc(idx=2)]), _tool("c2", "r2"),
            _user("Task A"),
            _asst("a_a", tool_calls=[_tc(idx=3)]), _tool("c3", "r_a"),
            _user("Task B"),
            _asst("a_b", tool_calls=[_tc(idx=4)]), _tool("c4", "r_b"),
        ]
        view, compacted, _ = build_compact_view(
            _make_test_encoding(), msgs, max_tokens=1,
            protect_first_rounds=2,
            protect_last_user_rounds=2,
        )
        validate_tool_call_integrity(view)
        user_texts = [m["content"] for m in view if m["role"] == "user"]
        assert "Task A" in user_texts
        assert "Task B" in user_texts
        tool_contents = [m.get("content", "") for m in view if m.get("role") == "tool"]
        assert "r0" in tool_contents, "r0 within protect_first=2 should be kept"
        assert "r_a" in tool_contents
        assert "r_b" in tool_contents
