"""BoardOrchestrator 单元测试 — 私董会讨论引擎"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from agentkit.experts.board import BoardTeam
from agentkit.experts.board_orchestrator import BoardOrchestrator
from agentkit.experts.config import ExpertConfig
from agentkit.experts.expert import Expert

from tests.unit.experts._helpers import make_chat_stream_mock


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


def _make_expert_config(
    name: str = "test_expert",
    is_lead: bool = False,
) -> ExpertConfig:
    """创建测试用 ExpertConfig"""
    return ExpertConfig(
        name=name,
        agent_type="expert",
        persona=f"测试专家 {name}",
        thinking_style="analytical",
        speaking_style="直接",
        decision_framework="分析",
        bound_skills=[],
        is_lead=is_lead,
        task_mode="llm_generate",
        prompt={"identity": name},
        avatar="T",
        color="#FF0000",
    )


def _make_mock_expert(
    name: str = "test_expert",
    is_lead: bool = False,
    is_active: bool = True,
) -> MagicMock:
    """创建 mock Expert 实例"""
    config = _make_expert_config(name, is_lead)
    expert = MagicMock(spec=Expert)
    expert.config = config
    expert.is_active = is_active
    expert.agent = MagicMock()
    expert.agent._llm_gateway = MagicMock()
    return expert


def _make_mock_gateway(response_content: str = "测试回复") -> AsyncMock:
    """创建 mock LLM gateway"""
    gateway = AsyncMock()
    response = MagicMock()
    response.content = response_content
    gateway.chat = AsyncMock(return_value=response)
    # board_orchestrator._stream_expert_speech() calls gateway.chat_stream()
    # (async gen). Without this, AsyncMock returns a coroutine lacking __aiter__,
    # triggering a RuntimeWarning + falling back to non-streaming path.
    gateway.chat_stream = make_chat_stream_mock(response_content)
    return gateway


def _setup_team_with_experts(
    team: BoardTeam,
    experts: list[MagicMock],
    moderator_name: str | None = None,
) -> None:
    """设置 BoardTeam 的内部专家字典"""
    for expert in experts:
        team._experts[expert.config.name] = expert
    if moderator_name:
        team._moderator_name = moderator_name
    elif experts:
        team._moderator_name = experts[0].config.name


# ── BoardOrchestrator 初始化测试 ──────────────────────────


class TestBoardOrchestratorInit:
    """BoardOrchestrator 初始化测试"""

    def test_init(self):
        """初始化"""
        team = BoardTeam()
        orchestrator = BoardOrchestrator(team=team)
        assert orchestrator._team is team

    def test_stop_commands(self):
        """停止命令集合"""
        assert "/stop" in BoardOrchestrator.STOP_COMMANDS
        assert "停止讨论" in BoardOrchestrator.STOP_COMMANDS
        assert "stop" in BoardOrchestrator.STOP_COMMANDS
        assert "结束讨论" in BoardOrchestrator.STOP_COMMANDS


# ── BoardOrchestrator.execute 测试 ────────────────────────


class TestBoardOrchestratorExecute:
    """BoardOrchestrator.execute 执行流程测试"""

    @pytest.mark.asyncio
    async def test_execute_no_active_experts(self):
        """无活跃专家时返回失败"""
        team = BoardTeam()
        orchestrator = BoardOrchestrator(team=team)

        result = await orchestrator.execute("测试主题")

        assert result["status"] == "failed"
        assert "error" in result
        assert "No active expert" in result["error"]

    @pytest.mark.asyncio
    async def test_execute_success(self):
        """正常执行流程"""
        team = BoardTeam(max_rounds=2)
        moderator = _make_mock_expert("moderator", is_lead=True)
        member1 = _make_mock_expert("member1")
        member2 = _make_mock_expert("member2")

        _setup_team_with_experts(team, [moderator, member1, member2], "moderator")

        # Mock LLM gateway
        gateway = _make_mock_gateway("测试发言内容")
        moderator.agent._llm_gateway = gateway
        member1.agent._llm_gateway = gateway
        member2.agent._llm_gateway = gateway

        orchestrator = BoardOrchestrator(team=team)

        # Mock broadcast_event to avoid transport issues
        with patch.object(orchestrator, "_broadcast_event", new_callable=AsyncMock):
            result = await orchestrator.execute("测试主题")

        assert result["status"] == "completed"
        assert result["total_rounds"] == 2
        assert "summary" in result

    @pytest.mark.asyncio
    async def test_execute_with_stop_command(self):
        """用户发送停止命令时终止讨论"""
        team = BoardTeam(max_rounds=5)
        moderator = _make_mock_expert("moderator", is_lead=True)
        member1 = _make_mock_expert("member1")

        _setup_team_with_experts(team, [moderator, member1], "moderator")

        gateway = _make_mock_gateway("测试内容")
        moderator.agent._llm_gateway = gateway
        member1.agent._llm_gateway = gateway

        # 添加停止命令作为用户干预
        await team.add_user_intervention("/stop")

        orchestrator = BoardOrchestrator(team=team)

        with patch.object(orchestrator, "_broadcast_event", new_callable=AsyncMock):
            result = await orchestrator.execute("测试主题")

        # 应该在第1轮就停止（因为 consume_user_interventions 在循环开始时检查）
        assert result["status"] == "completed"
        assert result["total_rounds"] <= 1

    @pytest.mark.asyncio
    async def test_execute_llm_unavailable_uses_fallback(self):
        """LLM 不可用时使用回退文本，仍正常完成"""
        team = BoardTeam(max_rounds=2)
        moderator = _make_mock_expert("moderator", is_lead=True)
        _setup_team_with_experts(team, [moderator], "moderator")

        # Mock gateway to raise exception for all LLM calls
        gateway = AsyncMock()
        gateway.chat = AsyncMock(side_effect=Exception("LLM 不可用"))
        moderator.agent._llm_gateway = gateway

        orchestrator = BoardOrchestrator(team=team)

        with patch.object(orchestrator, "_broadcast_event", new_callable=AsyncMock):
            result = await orchestrator.execute("测试主题")

        # LLM 不可用时，orchestrator 使用回退文本，仍正常完成
        assert result["status"] == "completed"
        assert result["total_rounds"] == 2
        # summary 应包含回退文本
        assert "summary" in result

    @pytest.mark.asyncio
    async def test_execute_unexpected_exception(self):
        """非 LLM 异常时进入回退处理"""
        team = BoardTeam(max_rounds=2)
        moderator = _make_mock_expert("moderator", is_lead=True)
        _setup_team_with_experts(team, [moderator], "moderator")

        # Provide a working gateway for fallback conclusion
        gateway = _make_mock_gateway("回退总结")
        moderator.agent._llm_gateway = gateway

        orchestrator = BoardOrchestrator(team=team)

        # Mock _generate_moderator_opening to raise an unexpected exception
        with patch.object(
            orchestrator,
            "_generate_moderator_opening",
            new_callable=AsyncMock,
            side_effect=RuntimeError("Unexpected error"),
        ):
            with patch.object(orchestrator, "_broadcast_event", new_callable=AsyncMock):
                result = await orchestrator.execute("测试主题")

        # 非预期异常应返回 failed 状态
        assert result["status"] == "failed"
        assert "error" in result


# ── BoardOrchestrator._has_stop_command 测试 ──────────────


class TestBoardOrchestratorStopCommand:
    """BoardOrchestrator 停止命令检测测试"""

    def test_has_stop_command_true(self):
        """检测到停止命令"""
        team = BoardTeam()
        orchestrator = BoardOrchestrator(team=team)

        assert orchestrator._has_stop_command(["/stop"]) is True
        assert orchestrator._has_stop_command(["停止讨论"]) is True
        assert orchestrator._has_stop_command(["some text", "stop"]) is True
        assert orchestrator._has_stop_command(["结束讨论"]) is True

    def test_has_stop_command_false(self):
        """无停止命令"""
        team = BoardTeam()
        orchestrator = BoardOrchestrator(team=team)

        assert orchestrator._has_stop_command([]) is False
        assert orchestrator._has_stop_command(["继续讨论"]) is False
        assert orchestrator._has_stop_command(["请多说一些"]) is False

    def test_has_stop_command_case_insensitive(self):
        """停止命令大小写不敏感"""
        team = BoardTeam()
        orchestrator = BoardOrchestrator(team=team)

        assert orchestrator._has_stop_command(["STOP"]) is True
        assert orchestrator._has_stop_command(["Stop"]) is True


# ── BoardOrchestrator._get_llm_gateway 测试 ───────────────


class TestBoardOrchestratorGetGateway:
    """BoardOrchestrator._get_llm_gateway 测试"""

    def test_get_gateway_from_expert(self):
        """从指定专家获取 gateway"""
        team = BoardTeam()
        orchestrator = BoardOrchestrator(team=team)

        expert = _make_mock_expert("test")
        gateway = orchestrator._get_llm_gateway(expert)

        assert gateway is not None

    def test_get_gateway_fallback_to_active_experts(self):
        """从其他活跃专家回退获取 gateway"""
        team = BoardTeam()
        moderator = _make_mock_expert("moderator", is_lead=True)
        # moderator 没有 gateway
        moderator.agent._llm_gateway = None

        member = _make_mock_expert("member")
        _setup_team_with_experts(team, [moderator, member], "moderator")

        orchestrator = BoardOrchestrator(team=team)

        # 应该从 member 回退获取
        gateway = orchestrator._get_llm_gateway(moderator)
        assert gateway is not None

    def test_get_gateway_none_when_no_gateway(self):
        """无可用 gateway 时返回 None"""
        team = BoardTeam()
        moderator = _make_mock_expert("moderator", is_lead=True)
        moderator.agent._llm_gateway = None
        _setup_team_with_experts(team, [moderator], "moderator")

        orchestrator = BoardOrchestrator(team=team)
        gateway = orchestrator._get_llm_gateway(moderator)
        assert gateway is None


# ── BoardOrchestrator._broadcast_event 测试 ───────────────


class TestBoardOrchestratorBroadcast:
    """BoardOrchestrator._broadcast_event 测试"""

    @pytest.mark.asyncio
    async def test_broadcast_event_with_transport(self):
        """有 transport 时广播事件"""
        team = BoardTeam()
        orchestrator = BoardOrchestrator(team=team)

        # Mock transport
        team._handoff_transport = AsyncMock()
        team._handoff_transport.send = AsyncMock()

        await orchestrator._broadcast_event("board_started", {"topic": "测试"})

        team._handoff_transport.send.assert_called_once()
        call_args = team._handoff_transport.send.call_args
        assert call_args[0][0] == team.team_channel
        assert call_args[0][1]["type"] == "board_started"
        assert call_args[0][1]["topic"] == "测试"

    @pytest.mark.asyncio
    async def test_broadcast_event_no_transport(self):
        """无 transport 时不报错"""
        team = BoardTeam()
        team._handoff_transport = None
        orchestrator = BoardOrchestrator(team=team)

        # 不应抛出异常
        await orchestrator._broadcast_event("board_started", {"topic": "测试"})

    @pytest.mark.asyncio
    async def test_broadcast_event_transport_error(self):
        """transport 错误时不传播异常"""
        team = BoardTeam()
        orchestrator = BoardOrchestrator(team=team)

        team._handoff_transport = AsyncMock()
        team._handoff_transport.send = AsyncMock(side_effect=Exception("Transport error"))

        # 不应抛出异常
        await orchestrator._broadcast_event("board_started", {"topic": "测试"})
