"""ExpertTeam 用户干预通道 + TeamOrchestrator 干预处理单元测试 (U4)

测试覆盖：
- ExpertTeam 干预队列
  * add_user_intervention → consume_user_interventions 返回消息
  * 多条干预消息累积，一次性消费
  * consume 后队列清空，再次 consume 返回空
  * broadcast_user_message 同时入队干预队列
- TeamOrchestrator._process_interventions
  * /stop → 返回 True（终止执行）+ 广播 plan_update
  * /debate <topic> → 插入 DEBATE phase + 广播 plan_update
  * /debate 受 MAX_DEBATES 限制
  * /debate 无 topic 时忽略
  * 普通文本 → 累积到 _user_context
  * 空干预队列 → 返回 False，无副作用
- 集成: _user_context 影响 synthesis prompt
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest

from agentkit.core.handoff_transport import InProcessHandoffTransport
from agentkit.experts.config import ExpertConfig
from agentkit.experts.orchestrator import TeamOrchestrator
from agentkit.experts.plan import PhaseStatus, PhaseType, PlanPhase, TeamPlan
from agentkit.experts.team import ExpertTeam


# ── 辅助函数 ──────────────────────────────────────────────


def _make_expert_config(name: str = "test_expert", is_lead: bool = False) -> ExpertConfig:
    return ExpertConfig(
        name=name,
        agent_type="expert",
        persona=f"{name}的角色描述",
        thinking_style="逻辑推理",
        speaking_style="简洁直接",
        decision_framework="数据驱动决策",
        bound_skills=["skill_a"],
        is_lead=is_lead,
        task_mode="llm_generate",
        prompt={"identity": "测试"},
    )


def _make_mock_expert(
    name: str = "test_expert",
    is_lead: bool = False,
    is_active: bool = True,
    gateway: MagicMock | None = None,
) -> MagicMock:
    config = _make_expert_config(name=name, is_lead=is_lead)
    expert = MagicMock()
    expert.config = config
    expert.is_active = is_active
    expert.team_id = None
    expert.get_capabilities_summary.return_value = {
        "name": name,
        "persona": config.persona,
        "thinking_style": config.thinking_style,
        "bound_skills": config.bound_skills,
        "is_lead": is_lead,
    }
    mock_agent = MagicMock()
    mock_agent._llm_gateway = gateway
    expert.agent = mock_agent
    return expert


def _make_team_with_experts(
    expert_names: list[str] | None = None,
    lead_name: str = "lead",
    gateway: MagicMock | None = None,
) -> ExpertTeam:
    team = ExpertTeam()
    transport = AsyncMock(spec=InProcessHandoffTransport)
    team._handoff_transport = transport

    if expert_names is None:
        expert_names = [lead_name, "member1", "member2"]

    for name in expert_names:
        is_lead = name == lead_name
        expert = _make_mock_expert(name=name, is_lead=is_lead, gateway=gateway)
        team._experts[name] = expert
        if is_lead:
            team._lead_expert_name = name

    return team


def _make_execution_phase(
    phase_id: str = "phase_1",
    name: str = "阶段一",
    assigned_expert: str = "member1",
    depends_on: list[str] | None = None,
    status: PhaseStatus = PhaseStatus.PENDING,
    result: dict | None = None,
) -> PlanPhase:
    return PlanPhase(
        id=phase_id,
        name=name,
        assigned_expert=assigned_expert,
        task_description=f"{name}的任务描述",
        depends_on=depends_on or [],
        phase_type=PhaseType.EXECUTION,
        status=status,
        result=result,
    )


def _make_plan(
    phases: list[PlanPhase],
    task: str = "测试任务",
    lead_expert: str = "lead",
) -> TeamPlan:
    return TeamPlan(
        id="test_plan",
        task=task,
        phases=phases,
        lead_expert=lead_expert,
    )


# ── ExpertTeam 干预队列测试 ──────────────────────────────


class TestExpertTeamInterventionQueue:
    """ExpertTeam 干预队列基础功能测试"""

    @pytest.mark.asyncio
    async def test_add_and_consume_intervention(self):
        """add_user_intervention → consume_user_interventions 返回消息"""
        team = ExpertTeam()
        team._handoff_transport = AsyncMock(spec=InProcessHandoffTransport)

        await team.add_user_intervention("/debate 前端框架选型")

        interventions = team.consume_user_interventions()
        assert interventions == ["/debate 前端框架选型"]

    @pytest.mark.asyncio
    async def test_multiple_interventions_accumulate(self):
        """多条干预消息累积，一次性消费"""
        team = ExpertTeam()
        team._handoff_transport = AsyncMock(spec=InProcessHandoffTransport)

        await team.add_user_intervention("第一条")
        await team.add_user_intervention("第二条")
        await team.add_user_intervention("第三条")

        interventions = team.consume_user_interventions()
        assert len(interventions) == 3
        assert interventions[0] == "第一条"
        assert interventions[1] == "第二条"
        assert interventions[2] == "第三条"

    @pytest.mark.asyncio
    async def test_consume_clears_queue(self):
        """consume 后队列清空，再次 consume 返回空"""
        team = ExpertTeam()
        team._handoff_transport = AsyncMock(spec=InProcessHandoffTransport)

        await team.add_user_intervention("消息")

        first = team.consume_user_interventions()
        assert len(first) == 1

        second = team.consume_user_interventions()
        assert second == []

    def test_consume_empty_queue_returns_empty_list(self):
        """空队列 consume 返回空列表"""
        team = ExpertTeam()
        interventions = team.consume_user_interventions()
        assert interventions == []

    @pytest.mark.asyncio
    async def test_broadcast_user_message_enqueues_intervention(self):
        """broadcast_user_message 同时入队干预队列"""
        team = ExpertTeam()
        team._handoff_transport = AsyncMock(spec=InProcessHandoffTransport)

        await team.broadcast_user_message("测试消息")

        interventions = team.consume_user_interventions()
        assert interventions == ["测试消息"]

    @pytest.mark.asyncio
    async def test_add_user_intervention_broadcasts_to_channel(self):
        """add_user_intervention 广播到 team channel"""
        team = ExpertTeam()
        transport = AsyncMock(spec=InProcessHandoffTransport)
        team._handoff_transport = transport

        await team.add_user_intervention("/stop")

        assert transport.send.called
        call_args = transport.send.call_args
        channel = call_args[0][0]
        message = call_args[0][1]
        assert channel == team.team_channel
        assert message["type"] == "user_intervention"
        assert message["content"] == "/stop"


# ── TeamOrchestrator._process_interventions 测试 ────────


class TestProcessInterventionsStop:
    """_process_interventions /stop 处理测试"""

    @pytest.mark.asyncio
    async def test_stop_returns_true(self):
        """/stop → 返回 True（终止执行）"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        plan = _make_plan(phases=[_make_execution_phase()])

        await team.add_user_intervention("/stop")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is True

    @pytest.mark.asyncio
    async def test_stop_broadcasts_plan_update(self):
        """/stop → 广播 plan_update with stopped_by_user"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        plan = _make_plan(phases=[_make_execution_phase()])

        await team.add_user_intervention("/stop")

        await orchestrator._process_interventions(team.lead_expert, plan)

        transport = team._handoff_transport
        assert transport.send.called
        last_call = transport.send.call_args_list[-1]
        event_data = last_call[0][1]
        assert event_data["type"] == "plan_update"
        assert event_data["stopped_by_user"] is True

    @pytest.mark.asyncio
    async def test_stop_chinese_alias_works(self):
        """中文停止命令 '停止' 也能终止"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        plan = _make_plan(phases=[_make_execution_phase()])

        await team.add_user_intervention("停止")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is True


class TestProcessInterventionsDebate:
    """_process_interventions /debate 处理测试"""

    @pytest.mark.asyncio
    async def test_debate_inserts_debate_phase(self):
        """/debate <topic> → 插入 DEBATE phase"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        # 需要一个已完成的 phase 作为 anchor
        completed = _make_execution_phase(
            phase_id="p1", status=PhaseStatus.COMPLETED, result={"content": "结果"}
        )
        pending = _make_execution_phase(phase_id="p2", depends_on=["p1"])
        plan = _make_plan(phases=[completed, pending])

        await team.add_user_intervention("/debate 前端框架选型")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is False  # 不终止
        assert orchestrator._debate_count == 1
        # 应该新增一个 DEBATE phase
        debate_phases = [p for p in plan.phases if p.phase_type == PhaseType.DEBATE]
        assert len(debate_phases) == 1
        assert "前端框架选型" in debate_phases[0].debate_config["topic"]

    @pytest.mark.asyncio
    async def test_debate_broadcasts_plan_update(self):
        """/debate → 广播 plan_update with debate_inserted"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        completed = _make_execution_phase(
            phase_id="p1", status=PhaseStatus.COMPLETED, result={"content": "结果"}
        )
        plan = _make_plan(phases=[completed])

        await team.add_user_intervention("/debate 测试话题")

        await orchestrator._process_interventions(team.lead_expert, plan)

        transport = team._handoff_transport
        last_call = transport.send.call_args_list[-1]
        event_data = last_call[0][1]
        assert event_data["type"] == "plan_update"
        assert "debate_inserted" in event_data

    @pytest.mark.asyncio
    async def test_debate_respects_max_debates(self):
        """/debate 受 MAX_DEBATES 限制"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        orchestrator._debate_count = orchestrator.MAX_DEBATES
        completed = _make_execution_phase(
            phase_id="p1", status=PhaseStatus.COMPLETED, result={"content": "结果"}
        )
        plan = _make_plan(phases=[completed])

        await team.add_user_intervention("/debate 话题")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is False
        assert orchestrator._debate_count == orchestrator.MAX_DEBATES
        # 不应该新增 DEBATE phase
        debate_phases = [p for p in plan.phases if p.phase_type == PhaseType.DEBATE]
        assert len(debate_phases) == 0

    @pytest.mark.asyncio
    async def test_debate_without_topic_ignored(self):
        """/debate 无 topic 时忽略"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        plan = _make_plan(phases=[_make_execution_phase()])

        await team.add_user_intervention("/debate")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is False
        assert orchestrator._debate_count == 0

    @pytest.mark.asyncio
    async def test_debate_without_members_ignored(self):
        """/debate 无其他成员时忽略（只有 lead）"""
        team = _make_team_with_experts(expert_names=["lead"])
        orchestrator = TeamOrchestrator(team)
        completed = _make_execution_phase(
            phase_id="p1", status=PhaseStatus.COMPLETED, result={"content": "结果"}
        )
        plan = _make_plan(phases=[completed])

        await team.add_user_intervention("/debate 话题")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is False
        assert orchestrator._debate_count == 0


class TestProcessInterventionsPlainText:
    """_process_interventions 普通文本处理测试"""

    @pytest.mark.asyncio
    async def test_plain_text_accumulates_to_user_context(self):
        """普通文本 → 累积到 _user_context"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        plan = _make_plan(phases=[_make_execution_phase()])

        await team.add_user_intervention("请关注性能优化")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is False
        assert "请关注性能优化" in orchestrator._user_context

    @pytest.mark.asyncio
    async def test_multiple_plain_texts_accumulate(self):
        """多条普通文本都累积"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        plan = _make_plan(phases=[_make_execution_phase()])

        await team.add_user_intervention("第一条建议")
        await team.add_user_intervention("第二条建议")

        await orchestrator._process_interventions(team.lead_expert, plan)
        assert len(orchestrator._user_context) == 2
        assert "第一条建议" in orchestrator._user_context
        assert "第二条建议" in orchestrator._user_context

    @pytest.mark.asyncio
    async def test_user_context_influences_synthesis_prompt(self):
        """_user_context 被追加到 synthesis prompt"""
        # 用一个能捕获 prompt 的 gateway
        captured_prompt = []

        async def chat_side_effect(messages, model=None, **kwargs):
            captured_prompt.append(messages[0]["content"])
            response = MagicMock()
            response.content = "综合结果"
            return response

        gateway = AsyncMock()
        gateway.chat = AsyncMock(side_effect=chat_side_effect)
        team = _make_team_with_experts(gateway=gateway)
        orchestrator = TeamOrchestrator(team)
        orchestrator._user_context.append("请重点关注安全性")

        phases = [
            _make_execution_phase(
                phase_id="p1",
                name="阶段A",
                status=PhaseStatus.COMPLETED,
                result={"content": "结果A"},
            ),
            _make_execution_phase(
                phase_id="p2",
                name="阶段B",
                status=PhaseStatus.COMPLETED,
                result={"content": "结果B"},
            ),
        ]

        await orchestrator._synthesize_results(team.lead_expert, "任务", phases)

        assert len(captured_prompt) == 1
        assert "请重点关注安全性" in captured_prompt[0]
        assert "用户在执行期间补充的指导意见" in captured_prompt[0]


class TestProcessInterventionsEmpty:
    """_process_interventions 空队列测试"""

    @pytest.mark.asyncio
    async def test_empty_interventions_returns_false(self):
        """空干预队列 → 返回 False，无副作用"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        plan = _make_plan(phases=[_make_execution_phase()])

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is False
        assert orchestrator._debate_count == 0
        assert orchestrator._user_context == []


class TestProcessInterventionsMixed:
    """_process_interventions 混合消息测试"""

    @pytest.mark.asyncio
    async def test_mixed_messages_processed_in_order(self):
        """混合消息按顺序处理：文本 + debate + 文本"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        completed = _make_execution_phase(
            phase_id="p1", status=PhaseStatus.COMPLETED, result={"content": "结果"}
        )
        plan = _make_plan(phases=[completed])

        await team.add_user_intervention("先补充个上下文")
        await team.add_user_intervention("/debate 架构选型")
        await team.add_user_intervention("再补充一条")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is False
        # debate 插入了
        assert orchestrator._debate_count == 1
        # 两条普通文本都累积了
        assert len(orchestrator._user_context) == 2
        assert "先补充个上下文" in orchestrator._user_context
        assert "再补充一条" in orchestrator._user_context

    @pytest.mark.asyncio
    async def test_stop_terminates_even_with_other_messages(self):
        """混合消息中 /stop 终止执行（即使前面有其他消息）"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)
        completed = _make_execution_phase(
            phase_id="p1", status=PhaseStatus.COMPLETED, result={"content": "结果"}
        )
        plan = _make_plan(phases=[completed])

        await team.add_user_intervention("/debate 话题")
        await team.add_user_intervention("/stop")

        result = await orchestrator._process_interventions(team.lead_expert, plan)
        assert result is True
        # debate 在 stop 之前处理了
        assert orchestrator._debate_count == 1
