"""TeamOrchestrator 单元测试 (流水线模式)

测试覆盖：
- 流水线执行（阶段依赖、拓扑排序）
- 并行阶段执行
- 上下文隔离（独立 Agent 实例）
- SharedWorkspace 数据传递
- 阶段失败与依赖失败传播
- 全失败 fallback
- 事件广播（phase_started/phase_completed/phase_failed）
- 模型路由（_get_model）
- LLM 分解失败容错
- 循环依赖检测
- 无效专家引用 fallback
- TeamStatus.PLANNING 状态流转
"""

from __future__ import annotations

import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from agentkit.core.handoff_transport import InProcessHandoffTransport
from agentkit.core.protocol import TaskResult, TaskStatus
from agentkit.experts.config import ExpertConfig
from agentkit.experts.expert import Expert
from agentkit.experts.orchestrator import TeamOrchestrator
from agentkit.experts.plan import PhaseStatus, PhaseType, PlanPhase, PlanStatus, TeamPlan
from agentkit.experts.team import ExpertTeam, TeamStatus


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


def _make_expert_config(
    name: str = "test_expert",
    is_lead: bool = False,
    llm: dict | None = None,
) -> ExpertConfig:
    """创建测试用 ExpertConfig"""
    return ExpertConfig(
        name=name,
        agent_type="expert",
        persona="测试专家",
        thinking_style="逻辑推理",
        bound_skills=["skill_a"],
        is_lead=is_lead,
        task_mode="llm_generate",
        prompt={"identity": "测试"},
        llm=llm,
    )


def _make_mock_expert(
    name: str = "test_expert",
    is_lead: bool = False,
    is_active: bool = True,
    llm: dict | None = None,
) -> MagicMock:
    """创建 mock Expert"""
    config = _make_expert_config(name=name, is_lead=is_lead, llm=llm)
    expert = MagicMock(spec=Expert)
    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.execute() to return a successful TaskResult
    mock_agent = MagicMock()
    mock_agent.execute = AsyncMock(return_value=TaskResult(
        task_id="test",
        agent_name=name,
        status=TaskStatus.COMPLETED.value,
        output_data={"content": f"Result from {name}"},
        error_message=None,
        started_at=None,
        completed_at=None,
    ))
    # No LLM gateway by default (tests single-phase path)
    mock_agent._llm_gateway = None
    expert.agent = mock_agent
    return expert


def _make_team_with_experts(
    expert_names: list[str] | None = None,
    lead_name: str = "lead",
    pool: MagicMock | None = None,
) -> ExpertTeam:
    """创建包含 mock experts 的 ExpertTeam"""
    team = ExpertTeam()
    transport = AsyncMock(spec=InProcessHandoffTransport)
    team._handoff_transport = transport
    if pool is not None:
        team._pool = pool

    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)
        team._experts[name] = expert
        if is_lead:
            team._lead_expert_name = name

    return team


def _make_mock_llm_gateway(
    phases: list[dict] | None = None,
    synthesis_content: str = "综合结果",
) -> MagicMock:
    """创建 mock LLM gateway.

    If phases is provided, the gateway returns a JSON array of phases for
    decomposition. Otherwise returns a simple response for synthesis.
    """
    gateway = AsyncMock()
    if phases:
        phases_json = json.dumps(phases)
        decomp_response = MagicMock()
        decomp_response.content = phases_json
        synth_response = MagicMock()
        synth_response.content = synthesis_content
        # U3: 分歧检测会在 decomposition 与 synthesis 之间插入额外的 LLM 调用，
        # 因此用函数式 side_effect：首次返回 decomposition，其余一律返回 synthesis。
        call_count = [0]

        async def chat_side_effect(messages, model=None, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                return decomp_response
            return synth_response

        gateway.chat = AsyncMock(side_effect=chat_side_effect)
    else:
        response = MagicMock()
        response.content = synthesis_content
        gateway.chat = AsyncMock(return_value=response)
    return gateway


def _make_mock_pool() -> MagicMock:
    """创建 mock AgentPool，模拟上下文隔离的 agent 创建"""
    pool = MagicMock()
    pool.create_agent = AsyncMock(side_effect=lambda config: MagicMock(
        execute=AsyncMock(return_value=TaskResult(
            task_id="test",
            agent_name=config.name,
            status=TaskStatus.COMPLETED.value,
            output_data={"content": f"Isolated result from {config.name}"},
            error_message=None,
            started_at=None,
            completed_at=None,
        ))
    ))
    pool.remove_agent = AsyncMock()
    return pool


# ── 流水线执行测试 ────────────────────────────────────────


class TestPipelineExecution:
    """流水线模式执行测试"""

    @pytest.mark.asyncio
    async def test_execute_single_phase_completes(self):
        """无 LLM 时，任务作为单个阶段执行完成"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        result = await orchestrator.execute("测试任务")

        assert result["status"] == "completed"
        assert "result" in result
        assert "phase_results" in result
        assert "plan" in result
        assert team.status == TeamStatus.COMPLETED

    @pytest.mark.asyncio
    async def test_execute_sets_team_status_completed(self):
        """执行完成后设置 team 状态为 COMPLETED"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        await orchestrator.execute("测试任务")

        assert team.status == TeamStatus.COMPLETED

    @pytest.mark.asyncio
    async def test_pipeline_sequential_execution(self):
        """3 阶段（A→B→C）按序执行"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        # LLM 分解为 3 个串行阶段
        gateway = _make_mock_llm_gateway(
            phases=[
                {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []},
                {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": ["A"]},
                {"name": "C", "assigned_expert": "member2", "task_description": "阶段C", "depends_on": ["B"]},
            ]
        )
        team._experts["lead"].agent._llm_gateway = gateway

        # Track execution order
        execution_order: list[str] = []
        for name in ["lead", "member1", "member2"]:
            original_execute = team._experts[name].agent.execute
            async def tracking_execute(task_msg, _orig=original_execute, _name=name):
                execution_order.append(_name)
                return await _orig(task_msg)
            team._experts[name].agent.execute = tracking_execute

        result = await orchestrator.execute("串行任务")

        assert result["status"] == "completed"
        plan = result["plan"]
        assert len(plan.phases) == 3
        # All phases should be completed
        for ph in plan.phases:
            assert ph.status == PhaseStatus.COMPLETED
        # Verify sequential execution order: A(lead) → B(member1) → C(member2)
        assert execution_order == ["lead", "member1", "member2"]

    @pytest.mark.asyncio
    async def test_pipeline_parallel_phases(self):
        """2 个无依赖阶段并行执行"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(
            phases=[
                {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []},
                {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": []},
                {"name": "C", "assigned_expert": "member2", "task_description": "阶段C", "depends_on": ["A", "B"]},
            ]
        )
        team._experts["lead"].agent._llm_gateway = gateway

        result = await orchestrator.execute("并行任务")

        assert result["status"] == "completed"
        plan = result["plan"]
        assert len(plan.phases) == 3
        # A and B should be in the same layer (parallel)
        layers = plan.topological_sort()
        assert len(layers) == 2  # [A, B], [C]
        assert len(layers[0]) == 2  # A and B parallel

    @pytest.mark.asyncio
    async def test_pipeline_emits_team_formed_event(self):
        """执行时广播 team_formed 事件"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        await orchestrator.execute("测试任务")

        calls = team._handoff_transport.send.call_args_list
        event_types = [c[0][1]["type"] for c in calls]
        assert "team_formed" in event_types

    @pytest.mark.asyncio
    async def test_pipeline_emits_plan_update_with_phases(self):
        """执行时广播 plan_update 事件（包含阶段列表）"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        await orchestrator.execute("测试任务")

        calls = team._handoff_transport.send.call_args_list
        plan_updates = [c for c in calls if c[0][1].get("type") == "plan_update"]
        assert len(plan_updates) >= 1
        assert "plan_phases" in plan_updates[0][0][1]

    @pytest.mark.asyncio
    async def test_pipeline_emits_team_synthesis_event(self):
        """执行完成时广播 team_synthesis 事件"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        await orchestrator.execute("测试任务")

        calls = team._handoff_transport.send.call_args_list
        event_types = [c[0][1]["type"] for c in calls]
        assert "team_synthesis" in event_types


# ── 阶段事件广播测试 ──────────────────────────────────────


class TestPhaseEvents:
    """阶段事件广播测试"""

    @pytest.mark.asyncio
    async def test_emits_phase_started_and_completed(self):
        """执行时广播 phase_started 和 phase_completed 事件"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        await orchestrator.execute("测试任务")

        calls = team._handoff_transport.send.call_args_list
        event_types = [c[0][1]["type"] for c in calls]
        assert "phase_started" in event_types
        assert "phase_completed" in event_types

    @pytest.mark.asyncio
    async def test_emits_expert_step_and_result(self):
        """执行时广播 expert_step 和 expert_result 事件"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        await orchestrator.execute("测试任务")

        calls = team._handoff_transport.send.call_args_list
        event_types = [c[0][1]["type"] for c in calls]
        assert "expert_step" in event_types
        assert "expert_result" in event_types

    @pytest.mark.asyncio
    async def test_phase_started_contains_depends_on(self):
        """phase_started 事件包含 depends_on 字段"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(
            phases=[
                {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []},
                {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": ["A"]},
            ]
        )
        team._experts["lead"].agent._llm_gateway = gateway

        await orchestrator.execute("依赖任务")

        calls = team._handoff_transport.send.call_args_list
        phase_started_events = [c[0][1] for c in calls if c[0][1].get("type") == "phase_started"]
        # Should have 2 phase_started events (A and B)
        assert len(phase_started_events) == 2
        # B's phase_started should have depends_on with A's id
        phase_b_event = next(e for e in phase_started_events if e["phase_name"] == "B")
        assert len(phase_b_event["depends_on"]) == 1

    @pytest.mark.asyncio
    async def test_phase_failed_event_on_failure(self):
        """阶段失败时广播 phase_failed 事件"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        # Make all agents fail to trigger phase_failed
        for expert in team._experts.values():
            expert.agent.execute = AsyncMock(side_effect=RuntimeError("Execution failed"))

        result = await orchestrator.execute("失败任务")

        # Should fallback since all phases failed
        assert result["status"] == "fallback"
        # phase_failed should be emitted (or phase status is FAILED in plan)
        plan = result["plan"]
        assert all(ph.status == PhaseStatus.FAILED for ph in plan.phases)


# ── LLM 任务分解测试 ──────────────────────────────────────


class TestTaskDecomposition:
    """LLM 任务分解测试"""

    @pytest.mark.asyncio
    async def test_llm_decomposes_task_into_phases(self):
        """LLM 将任务分解为多个阶段"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(
            phases=[
                {"name": "规划", "assigned_expert": "lead", "task_description": "设计架构", "depends_on": []},
                {"name": "前端", "assigned_expert": "member1", "task_description": "实现UI", "depends_on": ["规划"]},
                {"name": "后端", "assigned_expert": "member2", "task_description": "实现API", "depends_on": ["规划"]},
            ]
        )
        team._experts["lead"].agent._llm_gateway = gateway

        result = await orchestrator.execute("开发功能")

        assert result["status"] == "completed"
        plan = result["plan"]
        assert len(plan.phases) == 3
        assert plan.phases[0].name == "规划"
        assert plan.phases[1].name == "前端"
        assert plan.phases[2].name == "后端"

    @pytest.mark.asyncio
    async def test_decomposition_fallback_to_single_phase(self):
        """LLM 不可用时回退到单个阶段"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        # No LLM gateway — should fall back to single phase
        result = await orchestrator.execute("测试任务")

        assert result["status"] == "completed"
        plan = result["plan"]
        assert len(plan.phases) == 1

    @pytest.mark.asyncio
    async def test_llm_decomposition_invalid_json_falls_back(self):
        """LLM 返回无效 JSON 时回退到单阶段"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = AsyncMock()
        bad_response = MagicMock()
        bad_response.content = "这不是JSON"
        synth_response = MagicMock()
        synth_response.content = "综合结果"
        gateway.chat = AsyncMock(side_effect=[bad_response, synth_response])
        team._experts["lead"].agent._llm_gateway = gateway

        result = await orchestrator.execute("测试任务")

        assert result["status"] == "completed"
        plan = result["plan"]
        assert len(plan.phases) == 1

    @pytest.mark.asyncio
    async def test_parse_phases_valid_json(self):
        """_parse_phases 正确解析 JSON 数组"""
        content = json.dumps([
            {"name": "A", "assigned_expert": "member1", "task_description": "任务A", "depends_on": []},
            {"name": "B", "assigned_expert": "member2", "task_description": "任务B", "depends_on": ["A"]},
        ])
        phases = TeamOrchestrator._parse_phases(content, ["member1", "member2"], "lead")
        assert len(phases) == 2
        assert phases[0].name == "A"
        assert phases[0].assigned_expert == "member1"
        assert phases[1].name == "B"
        assert phases[1].assigned_expert == "member2"
        # B depends on A
        assert len(phases[1].depends_on) == 1
        assert phases[1].depends_on[0] == phases[0].id

    @pytest.mark.asyncio
    async def test_parse_phases_invalid_expert_falls_back_to_lead(self):
        """_parse_phases 对无效专家名回退到 lead"""
        content = json.dumps([
            {"name": "A", "assigned_expert": "nonexistent", "task_description": "任务A", "depends_on": []},
        ])
        phases = TeamOrchestrator._parse_phases(content, ["member1"], "lead")
        assert len(phases) == 1
        assert phases[0].assigned_expert == "lead"

    @pytest.mark.asyncio
    async def test_parse_phases_invalid_json_returns_empty(self):
        """_parse_phases 对无效 JSON 返回空列表"""
        phases = TeamOrchestrator._parse_phases("not json at all", ["member1"], "lead")
        assert phases == []

    @pytest.mark.asyncio
    async def test_parse_phases_empty_name_skipped(self):
        """_parse_phases 跳过空名称的阶段"""
        content = json.dumps([
            {"name": "", "assigned_expert": "member1", "task_description": "任务A", "depends_on": []},
            {"name": "B", "assigned_expert": "member1", "task_description": "任务B", "depends_on": []},
        ])
        phases = TeamOrchestrator._parse_phases(content, ["member1"], "lead")
        assert len(phases) == 1
        assert phases[0].name == "B"

    @pytest.mark.asyncio
    async def test_parse_phases_resolves_depends_on_by_name(self):
        """_parse_phases 通过名称解析 depends_on 为 ID"""
        content = json.dumps([
            {"name": "规划", "assigned_expert": "lead", "task_description": "设计", "depends_on": []},
            {"name": "实现", "assigned_expert": "member1", "task_description": "编码", "depends_on": ["规划"]},
        ])
        phases = TeamOrchestrator._parse_phases(content, ["lead", "member1"], "lead")
        assert len(phases) == 2
        # 实现 depends on 规划
        assert phases[1].depends_on == [phases[0].id]

    @pytest.mark.asyncio
    async def test_parse_phases_unknown_dependency_ignored(self):
        """_parse_phases 对未知依赖名称忽略"""
        content = json.dumps([
            {"name": "A", "assigned_expert": "lead", "task_description": "任务A", "depends_on": ["unknown_phase"]},
        ])
        phases = TeamOrchestrator._parse_phases(content, ["lead"], "lead")
        assert len(phases) == 1
        assert len(phases[0].depends_on) == 0  # unknown dependency ignored


# ── 阶段执行测试 ──────────────────────────────────────────


class TestPhaseExecution:
    """阶段执行测试"""

    @pytest.mark.asyncio
    async def test_phase_execution_calls_agent_execute(self):
        """阶段执行调用 agent.execute()"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        await orchestrator.execute("测试任务")

        # Lead expert's agent should have been called
        team._experts["lead"].agent.execute.assert_awaited()

    @pytest.mark.asyncio
    async def test_phase_marks_completed(self):
        """阶段执行后状态标记为 COMPLETED"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        result = await orchestrator.execute("测试任务")

        plan = result["plan"]
        for ph in plan.phases:
            assert ph.status == PhaseStatus.COMPLETED
            assert ph.result is not None

    @pytest.mark.asyncio
    async def test_phase_with_invalid_expert_falls_back_to_lead(self):
        """阶段分配的专家不可用时回退到 lead"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(
            phases=[
                {"name": "A", "assigned_expert": "nonexistent", "task_description": "任务A", "depends_on": []},
            ]
        )
        team._experts["lead"].agent._llm_gateway = gateway

        result = await orchestrator.execute("测试任务")

        assert result["status"] == "completed"
        plan = result["plan"]
        # The phase should have been reassigned to lead
        assert plan.phases[0].assigned_expert == "lead"

    @pytest.mark.asyncio
    async def test_shared_workspace_passing(self):
        """阶段 A 的输出写入 SharedWorkspace，阶段 B 能读取"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(
            phases=[
                {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []},
                {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": ["A"]},
            ]
        )
        team._experts["lead"].agent._llm_gateway = gateway

        result = await orchestrator.execute("依赖任务")

        assert result["status"] == "completed"
        # Verify workspace.write was called for each phase
        workspace = team.workspace
        # Check that phase outputs were written
        plan = result["plan"]
        for ph in plan.phases:
            key = f"{plan.id}/phase/{ph.id}/output"
            data = await workspace.read(key)
            assert data is not None
            assert "value" in data

    @pytest.mark.asyncio
    async def test_context_isolation_with_pool(self):
        """使用 AgentPool 时创建独立 agent 实例（上下文隔离）"""
        pool = _make_mock_pool()
        team = _make_team_with_experts(pool=pool)
        orchestrator = TeamOrchestrator(team)

        await orchestrator.execute("测试任务")

        # Pool.create_agent should have been called for context isolation
        pool.create_agent.assert_awaited()
        # Pool.remove_agent should have been called for cleanup
        pool.remove_agent.assert_awaited()

    @pytest.mark.asyncio
    async def test_context_isolation_fallback_without_pool(self):
        """无 AgentPool 时使用 expert 的现有 agent"""
        team = _make_team_with_experts()  # No pool
        orchestrator = TeamOrchestrator(team)

        result = await orchestrator.execute("测试任务")

        # Should still complete successfully using expert's existing agent
        assert result["status"] == "completed"
        team._experts["lead"].agent.execute.assert_awaited()


# ── 阶段失败与依赖传播测试 ────────────────────────────────


class TestPhaseFailure:
    """阶段失败与依赖传播测试"""

    @pytest.mark.asyncio
    async def test_phase_failure_marks_dependents_failed(self):
        """阶段 B 失败时，依赖 B 的阶段 C 标记为 FAILED"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(
            phases=[
                {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []},
                {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": ["A"]},
                {"name": "C", "assigned_expert": "member2", "task_description": "阶段C", "depends_on": ["B"]},
            ]
        )
        team._experts["lead"].agent._llm_gateway = gateway

        # Make member1's agent fail (phase B)
        team._experts["member1"].agent.execute = AsyncMock(side_effect=RuntimeError("B failed"))

        result = await orchestrator.execute("失败传播任务")

        # Should still complete (phase A succeeded)
        assert result["status"] == "completed"
        plan = result["plan"]
        # Phase A should be completed
        phase_a = next(ph for ph in plan.phases if ph.name == "A")
        assert phase_a.status == PhaseStatus.COMPLETED
        # Phase B should be failed
        phase_b = next(ph for ph in plan.phases if ph.name == "B")
        assert phase_b.status == PhaseStatus.FAILED
        # Phase C should be failed (dependency B failed)
        phase_c = next(ph for ph in plan.phases if ph.name == "C")
        assert phase_c.status == PhaseStatus.FAILED

    @pytest.mark.asyncio
    async def test_all_phases_fail_triggers_fallback(self):
        """所有阶段失败时触发 fallback"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        # Make all agents fail
        for expert in team._experts.values():
            expert.agent.execute = AsyncMock(side_effect=RuntimeError("Execution failed"))

        result = await orchestrator.execute("全失败任务")

        assert result["status"] == "fallback"
        assert result["plan"].status == PlanStatus.FALLBACK

    @pytest.mark.asyncio
    async def test_fallback_uses_lead_expert(self):
        """回退使用 lead expert 执行原始任务"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        call_count = 0

        async def mock_execute(task_msg):
            nonlocal call_count
            call_count += 1
            if task_msg.task_type == "team_phase":
                raise RuntimeError("Phase failed")
            # Fallback succeeds
            return TaskResult(
                task_id=task_msg.task_id,
                agent_name="lead",
                status=TaskStatus.COMPLETED.value,
                output_data={"content": "Fallback result"},
                error_message=None,
                started_at=None,
                completed_at=None,
            )

        team._experts["lead"].agent.execute = AsyncMock(side_effect=mock_execute)

        result = await orchestrator.execute("测试任务")

        assert result["status"] == "fallback"
        assert result["result"]["content"] == "Fallback result"

    @pytest.mark.asyncio
    async def test_no_active_experts_returns_failed(self):
        """没有活跃专家时返回 failed"""
        team = _make_team_with_experts()
        for expert in team._experts.values():
            expert.is_active = False
        orchestrator = TeamOrchestrator(team)

        result = await orchestrator.execute("测试任务")

        assert result["status"] == "failed"
        assert "error" in result


# ── 循环依赖检测测试 ──────────────────────────────────────


class TestCircularDependency:
    """循环依赖检测测试"""

    @pytest.mark.asyncio
    async def test_circular_dependency_triggers_fallback(self):
        """A→B→A 的循环依赖触发 fallback"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        # Manually create a plan with circular dependency
        # We need to mock _decompose_task to return phases with circular dep
        phase_a = PlanPhase(name="A", assigned_expert="lead", task_description="A")
        phase_b = PlanPhase(name="B", assigned_expert="member1", task_description="B")
        phase_a.depends_on = [phase_b.id]
        phase_b.depends_on = [phase_a.id]

        with patch.object(
            TeamOrchestrator,
            "_decompose_task",
            return_value=[phase_a, phase_b],
        ):
            result = await orchestrator.execute("循环依赖任务")

        # Should fallback due to ValueError from topological_sort
        assert result["status"] == "fallback"
        assert result["plan"].status == PlanStatus.FALLBACK


# ── 结果综合测试 ──────────────────────────────────────────


class TestResultSynthesis:
    """结果综合测试"""

    @pytest.mark.asyncio
    async def test_synthesize_single_result(self):
        """单个阶段结果直接返回"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        result = await orchestrator.execute("测试任务")

        assert result["status"] == "completed"
        final = result["result"]
        assert "content" in final
        assert final["strategy"] == "best"
        assert final["phases_completed"] == 1

    @pytest.mark.asyncio
    async def test_synthesize_multiple_results_with_llm(self):
        """多个阶段结果通过 LLM 综合"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(
            phases=[
                {"name": "A", "assigned_expert": "member1", "task_description": "阶段A", "depends_on": []},
                {"name": "B", "assigned_expert": "member2", "task_description": "阶段B", "depends_on": []},
            ],
            synthesis_content="综合结果",
        )
        team._experts["lead"].agent._llm_gateway = gateway

        result = await orchestrator.execute("复杂任务")

        assert result["status"] == "completed"
        final = result["result"]
        assert final["content"] == "综合结果"
        assert final["strategy"] == "best"
        assert final["phases_completed"] == 2

    @pytest.mark.asyncio
    async def test_synthesize_without_llm_concatenates(self):
        """无 LLM 时拼接所有结果"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = AsyncMock()
        decomp_response = MagicMock()
        decomp_response.content = json.dumps([
            {"name": "A", "assigned_expert": "member1", "task_description": "阶段A", "depends_on": []},
            {"name": "B", "assigned_expert": "member2", "task_description": "阶段B", "depends_on": []},
        ])
        # Synthesis call raises to force concatenation fallback
        gateway.chat = AsyncMock(
            side_effect=[decomp_response, RuntimeError("LLM unavailable")]
        )
        team._experts["lead"].agent._llm_gateway = gateway

        result = await orchestrator.execute("复杂任务")

        assert result["status"] == "completed"
        final = result["result"]
        assert "content" in final
        # Should contain both results concatenated
        assert "Result from member1" in final["content"]
        assert "Result from member2" in final["content"]


# ── 模型路由测试 ──────────────────────────────────────────


class TestModelRouting:
    """模型路由测试（_get_model）"""

    def test_get_model_default_when_no_llm_config(self):
        """无 llm 配置时返回 'default'"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        model = orchestrator._get_model()
        assert model == "default"

    def test_get_model_from_expert_config(self):
        """从 expert.config.llm 读取模型名"""
        team = _make_team_with_experts()
        # Set llm config on lead expert
        team._experts["lead"].config.llm = {"model": "gpt-4", "temperature": 0.7}
        orchestrator = TeamOrchestrator(team)

        model = orchestrator._get_model()
        assert model == "gpt-4"

    def test_get_model_falls_back_to_default_when_no_model_key(self):
        """llm 配置中无 model 键时回退到 'default'"""
        team = _make_team_with_experts()
        team._experts["lead"].config.llm = {"temperature": 0.5}
        orchestrator = TeamOrchestrator(team)

        model = orchestrator._get_model()
        assert model == "default"

    def test_get_model_uses_specific_expert(self):
        """_get_model 使用指定 expert 的配置"""
        team = _make_team_with_experts()
        team._experts["member1"].config.llm = {"model": "claude-3"}
        orchestrator = TeamOrchestrator(team)

        model = orchestrator._get_model(team._experts["member1"])
        assert model == "claude-3"


# ── TeamStatus.PLANNING 状态流转测试 ──────────────────────


class TestTeamStatusPlanning:
    """TeamStatus.PLANNING 状态流转测试（KTD6）"""

    def test_team_status_planning_exists(self):
        """TeamStatus.PLANNING 枚举值存在"""
        assert TeamStatus.PLANNING == "planning"

    @pytest.mark.asyncio
    async def test_execute_transitions_through_planning(self):
        """执行时经过 PLANNING 状态"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        # Track status transitions
        statuses_seen: list[str] = []

        original_set_status = team.set_status

        def tracking_set_status(status: TeamStatus) -> None:
            statuses_seen.append(status.value)
            original_set_status(status)

        team.set_status = tracking_set_status

        await orchestrator.execute("测试任务")

        # Should have transitioned through PLANNING
        assert "planning" in statuses_seen
        # Should end at COMPLETED
        assert statuses_seen[-1] == "completed"

    @pytest.mark.asyncio
    async def test_execute_transitions_to_executing_after_planning(self):
        """PLANNING 后转为 EXECUTING"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        statuses_seen: list[str] = []
        original_set_status = team.set_status

        def tracking_set_status(status: TeamStatus) -> None:
            statuses_seen.append(status.value)
            original_set_status(status)

        team.set_status = tracking_set_status

        await orchestrator.execute("测试任务")

        # PLANNING should come before EXECUTING
        planning_idx = statuses_seen.index("planning")
        executing_idx = statuses_seen.index("executing")
        assert planning_idx < executing_idx


# ── 事件广播基础设施测试 ──────────────────────────────────


class TestBroadcastEvent:
    """事件广播基础设施测试"""

    @pytest.mark.asyncio
    async def test_broadcast_event_sends_to_transport(self):
        """广播事件通过 handoff_transport 发送"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        await orchestrator._broadcast_event("test_event", {"key": "value"})

        team._handoff_transport.send.assert_awaited()
        call_args = team._handoff_transport.send.call_args
        assert call_args[0][0] == team._team_channel
        message = call_args[0][1]
        assert message["type"] == "test_event"
        assert message["key"] == "value"

    @pytest.mark.asyncio
    async def test_broadcast_event_no_transport(self):
        """没有 handoff_transport 时不报错"""
        team = _make_team_with_experts()
        team._handoff_transport = None
        orchestrator = TeamOrchestrator(team)

        # Should not raise
        await orchestrator._broadcast_event("test_event", {"key": "value"})

    @pytest.mark.asyncio
    async def test_broadcast_event_handles_transport_error(self):
        """handoff_transport 发送失败时不影响执行"""
        team = _make_team_with_experts()
        team._handoff_transport.send = AsyncMock(side_effect=RuntimeError("Transport error"))
        orchestrator = TeamOrchestrator(team)

        # Should not raise
        await orchestrator._broadcast_event("test_event", {"key": "value"})


# ── LLM Gateway 测试 ──────────────────────────────────────


class TestLLMGateway:
    """LLM Gateway 获取测试"""

    def test_get_llm_gateway_from_lead(self):
        """从 lead expert 获取 LLM gateway"""
        team = _make_team_with_experts()
        gateway = MagicMock()
        team._experts["lead"].agent._llm_gateway = gateway
        orchestrator = TeamOrchestrator(team)

        result = orchestrator._get_llm_gateway()
        assert result is gateway

    def test_get_llm_gateway_no_gateway(self):
        """没有 LLM gateway 时返回 None"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        result = orchestrator._get_llm_gateway()
        assert result is None

    def test_get_llm_gateway_fallback_to_active_expert(self):
        """lead 没有 gateway 时从其他活跃专家获取"""
        team = _make_team_with_experts()
        gateway = MagicMock()
        team._experts["member1"].agent._llm_gateway = gateway
        orchestrator = TeamOrchestrator(team)

        result = orchestrator._get_llm_gateway()
        assert result is gateway


# ── 并发限制测试 (U2) ─────────────────────────────────────


class _ConcurrencyTracker:
    """Track max concurrent executions for semaphore testing."""

    def __init__(self) -> None:
        self.current = 0
        self.max_seen = 0

    def acquire(self) -> None:
        self.current += 1
        self.max_seen = max(self.max_seen, self.current)

    def release(self) -> None:
        self.current -= 1


class TestConcurrencyLimit:
    """U2: 子代理并发限制 — Semaphore 包裹同层并行阶段和辩论参数生成"""

    @pytest.mark.asyncio
    async def test_three_phases_parallel_under_default_limit(self):
        """Happy path: 3 个阶段同层，默认 limit=3，全部并行执行"""
        team = _make_team_with_experts(expert_names=["lead", "m1", "m2"])
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(phases=[
            {"name": "A", "assigned_expert": "lead", "task_description": "A", "depends_on": []},
            {"name": "B", "assigned_expert": "m1", "task_description": "B", "depends_on": []},
            {"name": "C", "assigned_expert": "m2", "task_description": "C", "depends_on": []},
        ])
        team._experts["lead"].agent._llm_gateway = gateway

        tracker = _ConcurrencyTracker()

        async def tracking_execute_phase(phase, plan):
            tracker.acquire()
            await asyncio.sleep(0.02)  # Ensure overlap
            tracker.release()
            phase.status = PhaseStatus.COMPLETED
            phase.result = {"content": f"Result {phase.name}"}
            return phase.result

        orchestrator._execute_phase = tracking_execute_phase
        # Avoid divergence detection inserting debates
        orchestrator._check_divergence_and_insert_debates = AsyncMock(return_value=None)

        result = await orchestrator.execute("test")

        assert result["status"] == "completed"
        assert tracker.max_seen == 3  # All 3 ran in parallel

    @pytest.mark.asyncio
    async def test_five_phases_max_three_concurrent(self):
        """Edge case: 5 个阶段同层，验证最多 3 个同时执行"""
        names = ["lead", "m1", "m2", "m3", "m4"]
        team = _make_team_with_experts(expert_names=names)
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(phases=[
            {"name": f"P{i}", "assigned_expert": name, "task_description": f"P{i}", "depends_on": []}
            for i, name in enumerate(names)
        ])
        team._experts["lead"].agent._llm_gateway = gateway

        tracker = _ConcurrencyTracker()

        async def tracking_execute_phase(phase, plan):
            tracker.acquire()
            await asyncio.sleep(0.05)  # Ensure overlap
            tracker.release()
            phase.status = PhaseStatus.COMPLETED
            phase.result = {"content": f"Result {phase.name}"}
            return phase.result

        orchestrator._execute_phase = tracking_execute_phase
        orchestrator._check_divergence_and_insert_debates = AsyncMock(return_value=None)

        result = await orchestrator.execute("test")

        assert result["status"] == "completed"
        assert tracker.max_seen <= 3  # Never more than 3 concurrent
        assert tracker.max_seen >= 2  # At least some parallelism

    @pytest.mark.asyncio
    async def test_max_concurrent_one_serializes(self):
        """Edge case: max_concurrent_phases=1 时退化为串行执行"""
        team = _make_team_with_experts(expert_names=["lead", "m1", "m2"])
        orchestrator = TeamOrchestrator(team, max_concurrent_phases=1)

        gateway = _make_mock_llm_gateway(phases=[
            {"name": "A", "assigned_expert": "lead", "task_description": "A", "depends_on": []},
            {"name": "B", "assigned_expert": "m1", "task_description": "B", "depends_on": []},
            {"name": "C", "assigned_expert": "m2", "task_description": "C", "depends_on": []},
        ])
        team._experts["lead"].agent._llm_gateway = gateway

        tracker = _ConcurrencyTracker()

        async def tracking_execute_phase(phase, plan):
            tracker.acquire()
            await asyncio.sleep(0.02)
            tracker.release()
            phase.status = PhaseStatus.COMPLETED
            phase.result = {"content": f"Result {phase.name}"}
            return phase.result

        orchestrator._execute_phase = tracking_execute_phase
        orchestrator._check_divergence_and_insert_debates = AsyncMock(return_value=None)

        result = await orchestrator.execute("test")

        assert result["status"] == "completed"
        assert tracker.max_seen == 1  # Strictly serial

    @pytest.mark.asyncio
    async def test_phase_failure_releases_semaphore(self):
        """Error path: 某阶段失败不影响 semaphore 释放（async with 保证释放）"""
        team = _make_team_with_experts(expert_names=["lead", "m1", "m2"])
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(phases=[
            {"name": "A", "assigned_expert": "lead", "task_description": "A", "depends_on": []},
            {"name": "B", "assigned_expert": "m1", "task_description": "B", "depends_on": []},
            {"name": "C", "assigned_expert": "m2", "task_description": "C", "depends_on": []},
        ])
        team._experts["lead"].agent._llm_gateway = gateway

        tracker = _ConcurrencyTracker()

        async def tracking_execute_phase(phase, plan):
            tracker.acquire()
            await asyncio.sleep(0.02)
            tracker.release()
            if phase.name == "A":
                raise RuntimeError("Phase A failed")
            phase.status = PhaseStatus.COMPLETED
            phase.result = {"content": f"Result {phase.name}"}
            return phase.result

        orchestrator._execute_phase = tracking_execute_phase
        orchestrator._check_divergence_and_insert_debates = AsyncMock(return_value=None)

        result = await orchestrator.execute("test")

        # B and C should still complete (fallback to single agent if needed)
        assert result["status"] == "completed"
        # Semaphore was released even after A failed
        assert tracker.max_seen <= 3

    @pytest.mark.asyncio
    async def test_debate_semaphore_limits_concurrency(self):
        """Integration: 辩论阶段 4 个专家并行，semaphore 限制生效

        ponytail: 直接调用 _execute_debate_phase（不经过 _bounded_phase），
        因此 phase 级 semaphore 未占用，4 个专家受 limit=3 限制 → 最多 3 并发。
        在完整流程中 _bounded_phase 会先占 1 个槽，辩论并发上限为 limit-1。
        """
        team = _make_team_with_experts(expert_names=["lead", "m1", "m2", "m3", "m4"])
        orchestrator = TeamOrchestrator(team)  # default limit=3

        phase = PlanPhase(
            id="debate_1",
            name="Debate",
            assigned_expert="lead",
            task_description="Test debate topic",
            depends_on=[],
            phase_type=PhaseType.DEBATE,
            debate_config={
                "topic": "Test topic",
                "participants": ["m1", "m2", "m3", "m4"],
                "max_rounds": 1,
            },
        )
        plan = TeamPlan(id="plan_1", task="test", phases=[phase])

        tracker = _ConcurrencyTracker()

        async def tracking_debate_argument(expert, topic, history, round_num):
            tracker.acquire()
            await asyncio.sleep(0.05)  # Ensure overlap
            tracker.release()
            return f"Argument from {expert.config.name}"

        orchestrator._generate_debate_opening = AsyncMock(return_value="Opening statement")
        orchestrator._generate_debate_argument = tracking_debate_argument
        orchestrator._generate_debate_summary = AsyncMock(return_value="Round summary")
        orchestrator._generate_debate_verdict = AsyncMock(return_value={
            "conclusion": "Final conclusion",
            "decision": "adopt",
            "rationale": "Because",
        })

        await orchestrator._execute_debate_phase(phase, plan)

        assert phase.status == PhaseStatus.COMPLETED
        # 4 experts, semaphore limit=3 → max 3 concurrent
        assert tracker.max_seen <= 3
        assert tracker.max_seen >= 2  # At least some parallelism


# ── U4: SharedWorkspace Redis 化 + 状态卸载测试 ─────────────


class TestSharedWorkspaceRedis:
    """U4: SharedWorkspace 状态卸载 — 阶段输出写入 workspace，内存只保留摘要"""

    def test_offload_result_creates_summary_with_ref(self):
        """_offload_result 返回摘要 + _ref_key + _offloaded=True"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        long_content = "x" * 1000
        result = orchestrator._offload_result(long_content, "plan/phase/p1/output")

        assert result["_offloaded"] is True
        assert result["_ref_key"] == "plan/phase/p1/output"
        assert len(result["content"]) < len(long_content)
        assert result["content"].endswith("...")

    def test_offload_result_short_content_no_truncation(self):
        """短内容不截断，但仍标记为 offloaded"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        short_content = "short result"
        result = orchestrator._offload_result(short_content, "ref_key")

        assert result["_offloaded"] is True
        assert result["content"] == short_content

    @pytest.mark.asyncio
    async def test_read_dependency_output_resolves_offloaded(self):
        """Happy path: offloaded result → 从 workspace 读取完整内容"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        # Write full content to workspace
        full_content = "x" * 1000
        ref_key = "plan/phase/p1/output"
        await team.workspace.write(ref_key, full_content, "lead")

        # Create a phase with offloaded result
        dep_phase = PlanPhase(
            id="p1",
            name="Phase1",
            assigned_expert="lead",
            task_description="test",
            depends_on=[],
        )
        dep_phase.status = PhaseStatus.COMPLETED
        dep_phase.result = orchestrator._offload_result(full_content, ref_key)

        # Read should return full content from workspace
        content = await orchestrator._read_dependency_output(dep_phase)
        assert content == full_content

    @pytest.mark.asyncio
    async def test_read_dependency_output_falls_back_on_workspace_failure(self):
        """Edge case: workspace 读取失败 → 降级到内存摘要"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        dep_phase = PlanPhase(
            id="p1",
            name="Phase1",
            assigned_expert="lead",
            task_description="test",
            depends_on=[],
        )
        dep_phase.status = PhaseStatus.COMPLETED
        dep_phase.result = {
            "content": "summary content",
            "_ref_key": "nonexistent_key",
            "_offloaded": True,
        }

        # Workspace read returns None (key doesn't exist) → fallback to summary
        content = await orchestrator._read_dependency_output(dep_phase)
        assert content == "summary content"

    @pytest.mark.asyncio
    async def test_read_dependency_output_non_offloaded_returns_directly(self):
        """Non-offloaded result → 直接返回 content"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        dep_phase = PlanPhase(
            id="p1",
            name="Phase1",
            assigned_expert="lead",
            task_description="test",
            depends_on=[],
        )
        dep_phase.status = PhaseStatus.COMPLETED
        dep_phase.result = {"content": "direct content"}

        content = await orchestrator._read_dependency_output(dep_phase)
        assert content == "direct content"

    @pytest.mark.asyncio
    async def test_team_accepts_redis_client(self):
        """ExpertTeam 创建时接收 redis_client 参数"""
        mock_redis = AsyncMock()
        team = ExpertTeam(redis_client=mock_redis)

        assert team._workspace._redis is mock_redis

    @pytest.mark.asyncio
    async def test_team_without_redis_uses_memory_dict(self):
        """ExpertTeam 无 redis_client → 使用内存 dict"""
        team = ExpertTeam()

        assert team._workspace._redis is None
        assert team._workspace._local_store == {}

    @pytest.mark.asyncio
    async def test_pipeline_offloads_large_results(self):
        """Integration: 流水线执行后，大输出被卸载到 workspace"""
        team = _make_team_with_experts()
        orchestrator = TeamOrchestrator(team)

        gateway = _make_mock_llm_gateway(phases=[
            {"name": "A", "assigned_expert": "lead", "task_description": "A", "depends_on": []},
            {"name": "B", "assigned_expert": "member1", "task_description": "B", "depends_on": ["A"]},
        ])
        team._experts["lead"].agent._llm_gateway = gateway

        # Mock _execute_phase to return large content + verify offloading
        large_content = "x" * 1000

        async def mock_execute_phase(phase, plan):
            phase.status = PhaseStatus.COMPLETED
            # Simulate the offloading that happens in _execute_execution_phase
            output_key = f"{plan.id}/phase/{phase.id}/output"
            await team.workspace.write(output_key, large_content, phase.assigned_expert)
            phase.result = orchestrator._offload_result(large_content, output_key)
            return phase.result

        orchestrator._execute_phase = mock_execute_phase
        orchestrator._check_divergence_and_insert_debates = AsyncMock(return_value=None)

        result = await orchestrator.execute("test")

        assert result["status"] == "completed"
        # Verify phases have offloaded results
        plan = result["plan"]
        for ph in plan.phases:
            if ph.status == PhaseStatus.COMPLETED:
                assert ph.result.get("_offloaded") is True
                assert len(ph.result["content"]) < len(large_content)
