"""Broadcast claim 流程单元测试

覆盖 ticker.py 中的 record_broadcast_response 和 BroadcastRound 轮次逻辑。
场景 B1-B8。
"""

import pytest
from unittest.mock import MagicMock, patch

pytestmark = pytest.mark.unit

from src.daemon.ticker import Ticker, BroadcastRound


# ---------------------------------------------------------------------------
# 辅助：构造最小 Ticker（只初始化 _broadcast_tracker）
# ---------------------------------------------------------------------------

def _make_minimal_ticker() -> MagicMock:
    """创建只包含 _broadcast_tracker 的 Ticker mock，绑定真实 record_broadcast_response"""
    ticker = MagicMock(spec=Ticker)
    ticker._broadcast_tracker = {}
    # 绑定真实实例方法
    ticker.record_broadcast_response = Ticker.record_broadcast_response.__get__(ticker, Ticker)
    return ticker


def _make_round(task_id: str = "task-1",
                notified: set = None,
                responded: set = None,
                round_number: int = 0) -> BroadcastRound:
    """快速构造 BroadcastRound"""
    br = BroadcastRound(task_id=task_id)
    br.notified_agents = notified if notified is not None else set()
    br.responded_agents = responded if responded is not None else set()
    br.round_number = round_number
    return br


# ===================================================================
# B1: agent claimed → tracker 被清理
# ===================================================================

class TestClaimedClearsTracker:
    def test_claimed_removes_tracker(self):
        """B1: outcome='claimed' 时 tracker 从 dict 中移除"""
        ticker = _make_minimal_ticker()
        ticker._broadcast_tracker["task-1"] = _make_round("task-1")

        ticker.record_broadcast_response("task-1", "agent-a", "claimed")

        assert "task-1" not in ticker._broadcast_tracker

    def test_claimed_ignores_responded(self):
        """B1 补充：claimed 不往 responded_agents 添加"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a", "agent-b"})
        ticker._broadcast_tracker["task-1"] = tracker

        ticker.record_broadcast_response("task-1", "agent-a", "claimed")

        assert "task-1" not in ticker._broadcast_tracker


# ===================================================================
# B2: agent no_reply → responded 增加
# ===================================================================

class TestNoReplyAddsResponded:
    def test_no_reply_adds_agent(self):
        """B2: outcome != 'claimed' 时 agent 加入 responded_agents"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a", "agent-b"})
        ticker._broadcast_tracker["task-1"] = tracker

        ticker.record_broadcast_response("task-1", "agent-a", "no_reply")

        assert "agent-a" in tracker.responded_agents
        assert "task-1" in ticker._broadcast_tracker  # 未清理

    def test_rejected_adds_agent(self):
        """B2 补充：'rejected' 也加入 responded（多 agent 避免轮次结束清空）"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a", "agent-b"})
        ticker._broadcast_tracker["task-1"] = tracker

        ticker.record_broadcast_response("task-1", "agent-a", "rejected")

        assert "agent-a" in tracker.responded_agents
        assert tracker.round_number == 0  # 还没完成一轮


# ===================================================================
# B3: 所有 notified 都 responded → round_number +1
# ===================================================================

class TestAllRespondedRoundCompletes:
    def test_all_responded_advances_round(self):
        """B3: notified_agents ⊆ responded_agents 时 round_number += 1，清空 sets"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a", "agent-b"}, round_number=0)
        ticker._broadcast_tracker["task-1"] = tracker

        ticker.record_broadcast_response("task-1", "agent-a", "no_reply")
        ticker.record_broadcast_response("task-1", "agent-b", "no_reply")

        assert tracker.round_number == 1
        assert len(tracker.notified_agents) == 0
        assert len(tracker.responded_agents) == 0

    def test_superset_responded_also_completes(self):
        """B3 补充：responded 超过 notified 也完成轮次"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a"}, round_number=0)
        ticker._broadcast_tracker["task-1"] = tracker

        # agent-a responded → matched → round completes
        ticker.record_broadcast_response("task-1", "agent-a", "no_reply")

        assert tracker.round_number == 1


# ===================================================================
# B4: 部分响应 → round 不结束
# ===================================================================

class TestPartialResponseNoRoundAdvance:
    def test_partial_response_keeps_round(self):
        """B4: 只有一个 agent 响应，另一个未响应 → round 不变"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a", "agent-b", "agent-c"}, round_number=0)
        ticker._broadcast_tracker["task-1"] = tracker

        ticker.record_broadcast_response("task-1", "agent-a", "no_reply")

        assert tracker.round_number == 0
        assert "agent-a" in tracker.responded_agents
        assert "task-1" in ticker._broadcast_tracker


# ===================================================================
# B5: 不存在的 task_id → 静默返回
# ===================================================================

class TestMissingTaskIdSilent:
    def test_missing_task_no_error(self):
        """B5: task_id 不在 tracker 中 → 不抛异常"""
        ticker = _make_minimal_ticker()

        # 不应抛异常
        ticker.record_broadcast_response("nonexistent-task", "agent-a", "claimed")
        ticker.record_broadcast_response("nonexistent-task", "agent-a", "no_reply")

        assert len(ticker._broadcast_tracker) == 0


# ===================================================================
# B6: round 2 正常完成
# ===================================================================

class TestRoundTwoCompletes:
    def test_second_round_completes(self):
        """B6: 完成 round 1 后再完成 round 2"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a"}, round_number=0)
        ticker._broadcast_tracker["task-1"] = tracker

        # Round 1
        ticker.record_broadcast_response("task-1", "agent-a", "no_reply")
        assert tracker.round_number == 1

        # Simulate: 下一轮 notified 被重新填充
        tracker.notified_agents = {"agent-b"}

        # Round 2
        ticker.record_broadcast_response("task-1", "agent-b", "no_reply")
        assert tracker.round_number == 2

    def test_three_rounds_sequentially(self):
        """B6 补充：连续三轮"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a"}, round_number=0)
        ticker._broadcast_tracker["task-1"] = tracker

        for expected_round in range(1, 4):
            ticker.record_broadcast_response("task-1", "agent-a", "no_reply")
            assert tracker.round_number == expected_round
            # 重新填充 notified（模拟下一轮广播）
            tracker.notified_agents = {"agent-a"}


# ===================================================================
# B7: 已 claimed 后其他 agent 仍回调 → 不影响
# ===================================================================

class TestClaimedThenOtherCallback:
    def test_other_agent_callback_after_claimed(self):
        """B7: agent-a 认领后 agent-b 回调 → tracker 已不存在，静默返回"""
        ticker = _make_minimal_ticker()
        tracker = _make_round("task-1", notified={"agent-a", "agent-b"})
        ticker._broadcast_tracker["task-1"] = tracker

        # agent-a claimed → tracker removed
        ticker.record_broadcast_response("task-1", "agent-a", "claimed")
        assert "task-1" not in ticker._broadcast_tracker

        # agent-b late callback → no crash
        ticker.record_broadcast_response("task-1", "agent-b", "no_reply")
        assert "task-1" not in ticker._broadcast_tracker


# ===================================================================
# B8: 3 轮后 _broadcast_claim 标记 escalated
# ===================================================================

class TestEscalationAfterThreeRounds:
    def test_round_3_triggers_escalation(self):
        """B8: round_number >= 3 时 _broadcast_claim 将任务放入 escalated 列表

        验证 tracker.round_number=3 时，任务在 _broadcast_claim 内部
        被识别为 escalated（tracker 被清理）。
        由于 _broadcast_claim 依赖大量基础设施，这里只验证判断条件。
        """
        ticker = _make_minimal_ticker()
        # 模拟 round_number = 3 的 tracker
        tracker = _make_round("task-1", round_number=3)
        ticker._broadcast_tracker["task-1"] = tracker

        # 验证条件：round_number >= 3 应被视为 escalated
        assert tracker.round_number >= 3

        # 模拟 _broadcast_claim 的分支逻辑
        escalated_ids = []
        broadcastable_ids = []
        for tid, tr in ticker._broadcast_tracker.items():
            if tr.round_number >= 3:
                escalated_ids.append(tid)
            else:
                broadcastable_ids.append(tid)

        assert "task-1" in escalated_ids
        assert "task-1" not in broadcastable_ids
