"""ExpertTeam 容器单元测试 (hub-and-spoke 模式)"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from agentkit.core.config_driven import ConfigDrivenAgent
from agentkit.core.handoff_transport import InProcessHandoffTransport
from agentkit.core.shared_workspace import SharedWorkspace
from agentkit.experts.config import ExpertConfig, ExpertTemplate
from agentkit.experts.expert import Expert
from agentkit.experts.registry import ExpertTemplateRegistry
from agentkit.experts.team import ExpertTeam, TeamStatus


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


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


def _make_mock_agent() -> MagicMock:
    """创建 mock ConfigDrivenAgent"""
    agent = MagicMock(spec=ConfigDrivenAgent)
    agent.name = "test_expert"
    agent._prompt_template = None
    return agent


def _make_mock_pool() -> AsyncMock:
    """创建 mock AgentPool"""
    pool = AsyncMock()
    pool.create_agent = AsyncMock(return_value=_make_mock_agent())
    pool.remove_agent = AsyncMock()
    return pool


def _make_mock_expert(
    name: str = "test_expert",
    is_lead: bool = False,
    is_active: bool = True,
) -> MagicMock:
    """创建 mock Expert"""
    config = _make_expert_config(name=name, is_lead=is_lead)
    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,
    }
    expert.destroy = AsyncMock()
    return expert


# ── ExpertTeam 创建测试 ───────────────────────────────────


class TestExpertTeamCreation:
    """ExpertTeam 初始化与默认值测试"""

    def test_default_values(self):
        """默认值：自动生成 team_id，FORMING 状态"""
        team = ExpertTeam()

        assert team.team_id is not None
        assert len(team.team_id) > 0
        assert team.status == TeamStatus.FORMING
        assert team.lead_expert is None
        assert team.experts == []
        assert team.active_experts == []

    def test_custom_team_id(self):
        """自定义 team_id"""
        team = ExpertTeam(team_id="my_team")

        assert team.team_id == "my_team"

    def test_custom_workspace(self):
        """自定义 SharedWorkspace"""
        workspace = SharedWorkspace()
        team = ExpertTeam(workspace=workspace)

        assert team._workspace is workspace

    def test_custom_pool(self):
        """自定义 AgentPool"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        assert team._pool is pool

    def test_custom_template_registry(self):
        """自定义 ExpertTemplateRegistry"""
        registry = ExpertTemplateRegistry()
        team = ExpertTeam(template_registry=registry)

        assert team._template_registry is registry


# ── ExpertTeam.create_team 测试 ────────────────────────────


class TestExpertTeamCreateTeam:
    """ExpertTeam.create_team 团队创建测试"""

    @pytest.mark.asyncio
    async def test_create_team_with_lead_only(self):
        """仅创建 Lead Expert"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        lead_config = _make_expert_config(name="lead", is_lead=True)

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            mock_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = mock_expert

            await team.create_team(lead_config)

        assert team._lead_expert_name == "lead"
        assert team.lead_expert is mock_expert
        assert team.status == TeamStatus.PLANNING
        assert mock_expert.team_id == team.team_id

    @pytest.mark.asyncio
    async def test_create_team_with_lead_and_members(self):
        """创建 Lead Expert 和成员 Expert"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        lead_config = _make_expert_config(name="lead", is_lead=True)
        member_config = _make_expert_config(name="member1", is_lead=False)

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            member_expert = _make_mock_expert(name="member1", is_lead=False)

            mock_create.side_effect = [lead_expert, member_expert]

            await team.create_team(lead_config, [member_config])

        assert len(team.experts) == 2
        assert team._lead_expert_name == "lead"
        assert team.status == TeamStatus.PLANNING

    @pytest.mark.asyncio
    async def test_create_team_without_pool_raises(self):
        """没有 AgentPool 时 create_team 抛出 RuntimeError"""
        team = ExpertTeam(pool=None)
        lead_config = _make_expert_config(name="lead", is_lead=True)

        with pytest.raises(RuntimeError, match="AgentPool not configured"):
            await team.create_team(lead_config)


# ── ExpertTeam.add_expert 测试 ─────────────────────────────


class TestExpertTeamAddExpert:
    """ExpertTeam.add_expert 动态添加 Expert 测试"""

    @pytest.mark.asyncio
    async def test_add_expert_with_config(self):
        """通过 ExpertConfig 添加 Expert"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        # 先创建团队
        lead_config = _make_expert_config(name="lead", is_lead=True)
        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert
            await team.create_team(lead_config)

        # 添加新成员
        new_config = _make_expert_config(name="new_member")
        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            new_expert = _make_mock_expert(name="new_member")
            mock_create.return_value = new_expert

            result = await team.add_expert(new_config)

        assert result is new_expert
        assert "new_member" in team._experts

    @pytest.mark.asyncio
    async def test_add_expert_with_template_name(self):
        """通过模板名称添加 Expert"""
        pool = _make_mock_pool()
        registry = ExpertTemplateRegistry()
        template_config = _make_expert_config(name="analyst", persona="分析师")
        registry.register(ExpertTemplate(name="analyst", config=template_config))

        team = ExpertTeam(pool=pool, template_registry=registry)

        # 先创建团队
        lead_config = _make_expert_config(name="lead", is_lead=True)
        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert
            await team.create_team(lead_config)

        # 通过模板名称添加
        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            analyst_expert = _make_mock_expert(name="analyst")
            mock_create.return_value = analyst_expert

            result = await team.add_expert("analyst")

        assert result is analyst_expert

    @pytest.mark.asyncio
    async def test_add_expert_with_nonexistent_template_raises(self):
        """使用不存在的模板名称添加 Expert 抛出 ValueError"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        # 先创建团队
        lead_config = _make_expert_config(name="lead", is_lead=True)
        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert
            await team.create_team(lead_config)

        with pytest.raises(ValueError, match="ExpertTemplate 'nonexistent' not found"):
            await team.add_expert("nonexistent")

    @pytest.mark.asyncio
    async def test_add_expert_broadcasts_joined_message(self):
        """添加 Expert 时广播 expert_joined 消息"""
        pool = _make_mock_pool()
        transport = AsyncMock(spec=InProcessHandoffTransport)
        team = ExpertTeam(pool=pool)
        team._handoff_transport = transport

        # 先创建团队
        lead_config = _make_expert_config(name="lead", is_lead=True)
        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert
            await team.create_team(lead_config)

        # 添加新成员
        new_config = _make_expert_config(name="new_member")
        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            new_expert = _make_mock_expert(name="new_member")
            mock_create.return_value = new_expert

            await team.add_expert(new_config)

        # 验证 broadcast 消息（create_team 也会调用 send，所以检查最后一次）
        calls = transport.send.call_args_list
        joined_calls = [
            c for c in calls if c[0][1].get("type") == "expert_joined"
        ]
        assert len(joined_calls) >= 1
        last_joined = joined_calls[-1]
        assert last_joined[0][1]["expert_name"] == "new_member"


# ── ExpertTeam.remove_expert 测试 ──────────────────────────


class TestExpertTeamRemoveExpert:
    """ExpertTeam.remove_expert 移除 Expert 测试"""

    @pytest.mark.asyncio
    async def test_remove_expert(self):
        """移除普通 Expert"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        lead_config = _make_expert_config(name="lead", is_lead=True)
        member_config = _make_expert_config(name="member1")

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            member_expert = _make_mock_expert(name="member1")
            mock_create.side_effect = [lead_expert, member_expert]
            await team.create_team(lead_config, [member_config])

        await team.remove_expert("member1")

        assert "member1" not in team._experts
        member_expert.destroy.assert_awaited_once_with(pool)

    @pytest.mark.asyncio
    async def test_remove_lead_expert_reassigns(self):
        """移除 Lead Expert 时重新分配给下一个活跃 Expert"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        lead_config = _make_expert_config(name="lead", is_lead=True)
        member_config = _make_expert_config(name="member1")

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            member_expert = _make_mock_expert(name="member1")
            mock_create.side_effect = [lead_expert, member_expert]
            await team.create_team(lead_config, [member_config])

        await team.remove_expert("lead")

        assert team._lead_expert_name == "member1"
        assert member_expert.config.is_lead is True

    @pytest.mark.asyncio
    async def test_remove_lead_expert_no_active_members(self):
        """移除 Lead Expert 且无其他活跃成员时 lead_expert_name 为 None"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        lead_config = _make_expert_config(name="lead", is_lead=True)

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert
            await team.create_team(lead_config)

        await team.remove_expert("lead")

        assert team._lead_expert_name is None

    @pytest.mark.asyncio
    async def test_remove_nonexistent_expert_no_error(self):
        """移除不存在的 Expert 不报错"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        # 不应抛出异常
        await team.remove_expert("nonexistent")

    @pytest.mark.asyncio
    async def test_remove_expert_broadcasts_left_message(self):
        """移除 Expert 时广播 expert_left 消息"""
        pool = _make_mock_pool()
        transport = AsyncMock(spec=InProcessHandoffTransport)
        team = ExpertTeam(pool=pool)
        team._handoff_transport = transport

        lead_config = _make_expert_config(name="lead", is_lead=True)
        member_config = _make_expert_config(name="member1")

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            member_expert = _make_mock_expert(name="member1")
            mock_create.side_effect = [lead_expert, member_expert]
            await team.create_team(lead_config, [member_config])

        await team.remove_expert("member1")

        # 验证 expert_left 消息
        calls = transport.send.call_args_list
        left_calls = [c for c in calls if c[0][1].get("type") == "expert_left"]
        assert len(left_calls) >= 1
        last_left = left_calls[-1]
        assert last_left[0][1]["expert_name"] == "member1"


# ── ExpertTeam.broadcast_user_message 测试 ─────────────────


class TestExpertTeamBroadcastUserMessage:
    """ExpertTeam.broadcast_user_message 用户干预消息广播测试"""

    @pytest.mark.asyncio
    async def test_broadcast_user_message(self):
        """广播用户干预消息到团队频道"""
        team = ExpertTeam()
        transport = AsyncMock(spec=InProcessHandoffTransport)
        team._handoff_transport = transport

        await team.broadcast_user_message("请暂停执行")

        transport.send.assert_awaited_once()
        call_args = transport.send.call_args
        assert call_args[0][0] == team._team_channel
        message = call_args[0][1]
        assert message["type"] == "user_intervention"
        assert message["content"] == "请暂停执行"
        assert "timestamp" in message


# ── ExpertTeam.get_shared_context 测试 ────────────────────


class TestExpertTeamGetSharedContext:
    """ExpertTeam.get_shared_context 共享上下文读取测试"""

    @pytest.mark.asyncio
    async def test_get_shared_context_reads_team_keys(self):
        """读取团队范围的共享上下文"""
        workspace = AsyncMock(spec=SharedWorkspace)
        workspace.list_keys = AsyncMock(
            return_value=[
                "team:abc:output1",
                "team:abc:output2",
                "other:key",
            ]
        )
        workspace.read = AsyncMock(
            side_effect=lambda key: {"value": f"data_{key}"} if key.startswith("team:abc") else None
        )

        team = ExpertTeam(team_id="abc", workspace=workspace)

        context = await team.get_shared_context()

        assert "team:abc:output1" in context
        assert "team:abc:output2" in context
        assert "other:key" not in context

    @pytest.mark.asyncio
    async def test_get_shared_context_empty(self):
        """没有团队范围的键时返回空字典"""
        workspace = AsyncMock(spec=SharedWorkspace)
        workspace.list_keys = AsyncMock(return_value=[])

        team = ExpertTeam(team_id="abc", workspace=workspace)

        context = await team.get_shared_context()

        assert context == {}


# ── ExpertTeam.dissolve 测试 ───────────────────────────────


class TestExpertTeamDissolve:
    """ExpertTeam.dissolve 团队解散测试"""

    @pytest.mark.asyncio
    async def test_dissolve_recycles_experts(self):
        """解散团队时回收所有 Expert"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        lead_config = _make_expert_config(name="lead", is_lead=True)
        member_config = _make_expert_config(name="member1")

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            member_expert = _make_mock_expert(name="member1")
            mock_create.side_effect = [lead_expert, member_expert]
            await team.create_team(lead_config, [member_config])

        await team.dissolve()

        assert team.status == TeamStatus.DISSOLVED
        assert team.experts == []
        assert team._lead_expert_name is None
        lead_expert.destroy.assert_awaited_once_with(pool)
        member_expert.destroy.assert_awaited_once_with(pool)

    @pytest.mark.asyncio
    async def test_dissolve_preserves_outputs_in_workspace(self):
        """解散团队后 SharedWorkspace 中的输出仍保留"""
        workspace = SharedWorkspace()
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool, workspace=workspace)

        # 写入一些数据到 workspace
        await workspace.write("team:abc:result", "重要输出", "lead")

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert

            lead_config = _make_expert_config(name="lead", is_lead=True)
            await team.create_team(lead_config)

        await team.dissolve()

        # workspace 数据仍然存在
        data = await workspace.read("team:abc:result")
        assert data is not None
        assert data["value"] == "重要输出"

    @pytest.mark.asyncio
    async def test_dissolve_closes_handoff_transport(self):
        """解散团队时关闭 handoff_transport"""
        pool = _make_mock_pool()
        transport = MagicMock(spec=InProcessHandoffTransport)
        transport.close = MagicMock()
        team = ExpertTeam(pool=pool)
        team._handoff_transport = transport

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert
            lead_config = _make_expert_config(name="lead", is_lead=True)
            await team.create_team(lead_config)

        await team.dissolve()

        transport.close.assert_called_once()


# ── ExpertTeam 操作已解散团队测试 ──────────────────────────


class TestExpertTeamDissolvedOperations:
    """解散后的团队操作应报错"""

    @pytest.mark.asyncio
    async def test_create_team_on_dissolved_raises(self):
        """在已解散的团队上 create_team 应报错（因为 pool 可能已被清理）"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert
            lead_config = _make_expert_config(name="lead", is_lead=True)
            await team.create_team(lead_config)

        await team.dissolve()

        # 解散后状态为 DISSOLVED
        assert team.status == TeamStatus.DISSOLVED


# ── ExpertTeam.lead_expert 属性测试 ────────────────────────


class TestExpertTeamLeadExpert:
    """ExpertTeam.lead_expert 属性测试"""

    @pytest.mark.asyncio
    async def test_lead_expert_returns_lead(self):
        """lead_expert 返回 Lead Expert"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        lead_config = _make_expert_config(name="lead", is_lead=True)

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            mock_create.return_value = lead_expert
            await team.create_team(lead_config)

        assert team.lead_expert is lead_expert

    def test_lead_expert_none_when_no_lead(self):
        """没有 Lead Expert 时返回 None"""
        team = ExpertTeam()

        assert team.lead_expert is None

    @pytest.mark.asyncio
    async def test_active_experts_filters_inactive(self):
        """active_experts 只返回活跃的 Expert"""
        pool = _make_mock_pool()
        team = ExpertTeam(pool=pool)

        lead_config = _make_expert_config(name="lead", is_lead=True)
        member_config = _make_expert_config(name="member1")

        with patch.object(Expert, "create", new_callable=AsyncMock) as mock_create:
            lead_expert = _make_mock_expert(name="lead", is_lead=True)
            member_expert = _make_mock_expert(name="member1")
            mock_create.side_effect = [lead_expert, member_expert]
            await team.create_team(lead_config, [member_config])

        # 标记 member1 为非活跃
        member_expert.is_active = False

        active = team.active_experts
        assert len(active) == 1
        assert active[0] is lead_expert


# ── ExpertTeam._build_team_context 测试 ────────────────────


class TestExpertTeamBuildContext:
    """ExpertTeam._build_team_context 团队上下文构建测试"""

    def test_build_team_context_with_lead_and_members(self):
        """构建包含 Lead 和成员的团队上下文"""
        team = ExpertTeam()

        lead_config = _make_expert_config(
            name="lead", persona="领导者", is_lead=True
        )
        member_config = _make_expert_config(
            name="analyst", persona="分析师", bound_skills=["data_query"]
        )

        context = team._build_team_context(lead_config, [member_config])

        assert "hub-and-spoke mode" in context
        assert "Lead Expert (hub): lead (领导者)" in context
        assert "Team Member (spoke): analyst (分析师)" in context
        assert "data_query" in context
        assert "depth=1" in context

    def test_build_team_context_no_lead(self):
        """没有 Lead Expert 时构建上下文"""
        team = ExpertTeam()
        member_config = _make_expert_config(name="analyst")

        context = team._build_team_context(None, [member_config])

        # 不应出现具体的 Lead Expert (hub): name 行
        assert "Lead Expert (hub):" not in context
        assert "Team Member (spoke): analyst" in context

    def test_build_team_context_skips_lead_in_members(self):
        """成员列表中包含 Lead 时跳过"""
        team = ExpertTeam()
        lead_config = _make_expert_config(name="lead", is_lead=True)

        context = team._build_team_context(lead_config, [lead_config])

        # Lead 不应出现在 Team Member 行
        assert "Team Member (spoke): lead" not in context
