"""F9 Agent 调度器单元测试 — 三级决策树 + 批量决策"""

import asyncio
import json
import pytest
from pathlib import Path
from typing import Any, Dict, Optional
from unittest.mock import AsyncMock, MagicMock

from src.blackboard.models import Task
from src.daemon.dispatcher import Dispatcher, DispatchLevel
from src.daemon.spawner import AgentBusyError

pytestmark = pytest.mark.unit


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def dispatcher():
    return Dispatcher(
        registered_agents=["zhangfei-dev", "guanyu-dev", "simayi-challenger"],
    )


@pytest.fixture
def task_pending():
    return Task(
        id="t1", title="Write code", status="pending",
        assigned_by="daemon", task_type="coding",
        assignee="zhangfei-dev",
    )


@pytest.fixture
def task_no_assignee():
    return Task(
        id="t2", title="Generic task", status="pending",
        assigned_by="daemon",
    )


@pytest.fixture
def task_unknown_agent():
    return Task(
        id="t3", title="Unknown agent task", status="pending",
        assigned_by="daemon", assignee="nonexistent-agent",
    )


# ---------------------------------------------------------------------------
# T1: 三级决策树
# ---------------------------------------------------------------------------

class TestDecisionTree:
    def test_local_action(self, dispatcher, task_pending):
        """L1 本地执行"""
        decision = dispatcher.decide(task_pending, "L1_guardrail")
        assert decision["level"] == DispatchLevel.LOCAL
        assert decision["agent_id"] == "daemon"

    def test_local_format_check(self, dispatcher, task_pending):
        """format_check → 本地"""
        decision = dispatcher.decide(task_pending, "format_check")
        assert decision["level"] == DispatchLevel.LOCAL

    def test_local_file_exists(self, dispatcher, task_pending):
        """file_exists_check → 本地"""
        decision = dispatcher.decide(task_pending, "file_exists_check")
        assert decision["level"] == DispatchLevel.LOCAL

    def test_registered_agent(self, dispatcher, task_pending):
        """注册 Agent → Full Agent"""
        decision = dispatcher.decide(task_pending)
        assert decision["level"] == DispatchLevel.FULL_AGENT
        assert decision["agent_id"] == "zhangfei-dev"

    def test_adjudication_new_session(self, dispatcher, task_pending):
        """adjudication → new_session=True"""
        decision = dispatcher.decide(task_pending, "adjudication")
        assert decision["level"] == DispatchLevel.FULL_AGENT
        assert decision["new_session"] is True

    def test_no_assignee_subagent(self, dispatcher, task_no_assignee):
        """无 assignee → 能力映射 fallback 庞统（Full Agent）"""
        decision = dispatcher.decide(task_no_assignee)
        assert decision["level"] == DispatchLevel.FULL_AGENT
        assert decision["agent_id"] == "pangtong-fujunshi"  # fallback

    def test_unknown_agent_escalate(self, dispatcher, task_unknown_agent):
        """未注册 Agent → 升级庞统"""
        decision = dispatcher.decide(task_unknown_agent)
        assert decision["level"] == DispatchLevel.ESCALATE
        assert decision["agent_id"] == "pangtong-fujunshi"
        assert decision["new_session"] is True

    def test_different_registered_agents(self, dispatcher):
        """不同注册 Agent 正确路由"""
        for agent_id in ["zhangfei-dev", "guanyu-dev", "simayi-challenger"]:
            task = Task(id="t", title="T", status="pending",
                        assigned_by="d", assignee=agent_id)
            decision = dispatcher.decide(task)
            assert decision["level"] == DispatchLevel.FULL_AGENT
            assert decision["agent_id"] == agent_id

    def test_unknown_action_with_registered_agent(self, dispatcher, task_pending):
        """未知 action_type 但有注册 assignee → Full Agent"""
        decision = dispatcher.decide(task_pending, "some_unknown_action")
        assert decision["level"] == DispatchLevel.FULL_AGENT


# ---------------------------------------------------------------------------
# T2: 调度执行（带 mock）
# ---------------------------------------------------------------------------

class TestDispatch:
    def test_dispatch_local(self, dispatcher, task_pending):
        """本地调度不需要 spawner"""
        result = asyncio.run(dispatcher.dispatch(task_pending, "L1_guardrail"))
        assert result["status"] == "dispatched"
        assert result["level"] == "local"

    def test_dispatch_full_agent(self, dispatcher, task_pending):
        """Full Agent 调度"""
        mock_spawner = MagicMock()
        mock_spawner.spawn_full_agent = AsyncMock(return_value="session-123")
        dispatcher.spawner = mock_spawner

        result = asyncio.run(dispatcher.dispatch(task_pending))
        assert result["status"] == "dispatched"
        assert result["session_id"] == "session-123"
        assert result["level"] == "full"

    def test_dispatch_no_spawner_returns_error(self, dispatcher, task_pending):
        """无 spawner → error"""
        result = asyncio.run(dispatcher.dispatch(task_pending))
        assert result["status"] == "error"
        assert "No spawner" in result["reason"]

    def test_dispatch_subagent(self, dispatcher, task_no_assignee):
        """无 assignee → 能力映射 fallback 庞统（Full Agent）"""
        mock_spawner = MagicMock()
        mock_spawner.spawn_full_agent = AsyncMock(return_value="auto-123")
        dispatcher.spawner = mock_spawner

        result = asyncio.run(dispatcher.dispatch(task_no_assignee))
        assert result["status"] == "dispatched"
        assert result["level"] == "full"
        assert result["agent_id"] == "pangtong-fujunshi"

    def test_dispatch_escalate(self, dispatcher, task_unknown_agent):
        """升级调度"""
        mock_spawner = MagicMock()
        mock_spawner.spawn_full_agent = AsyncMock(return_value="esc-123")
        dispatcher.spawner = mock_spawner

        result = asyncio.run(dispatcher.dispatch(task_unknown_agent))
        assert result["status"] == "dispatched"
        assert result["level"] == "escalate"

    def test_dispatch_spawn_failure(self, dispatcher, task_pending):
        """spawn 失败 → error"""
        mock_spawner = MagicMock()
        mock_spawner.spawn_full_agent = AsyncMock(side_effect=RuntimeError("spawn failed"))
        dispatcher.spawner = mock_spawner

        result = asyncio.run(dispatcher.dispatch(task_pending))
        assert result["status"] == "error"


# ---------------------------------------------------------------------------
# T3: 队列满拒绝
# ---------------------------------------------------------------------------

class TestConcurrencyControl:
    def test_counter_busy_skips(self, dispatcher, task_pending):
        """Agent 忙 → skip"""
        mock_spawner = MagicMock()
        mock_spawner.spawn_full_agent = AsyncMock(
            side_effect=AgentBusyError("zhangfei-dev", reason="counter_blocked")
        )
        dispatcher.spawner = mock_spawner

        result = asyncio.run(dispatcher.dispatch(task_pending))
        assert result["status"] == "skipped"
        assert "busy" in result["reason"].lower()

    def test_counter_releases_on_error(self, dispatcher, task_pending):
        """spawn 失败后释放 counter"""
        mock_spawner = MagicMock()
        mock_spawner.spawn_full_agent = AsyncMock(side_effect=RuntimeError("fail"))
        dispatcher.spawner = mock_spawner

        result = asyncio.run(dispatcher.dispatch(task_pending))
        assert result["status"] == "error"

    def test_local_not_blocked_by_counter(self, dispatcher, task_pending):
        """本地执行不受 counter 限制"""
        mock_counter = MagicMock()
        mock_counter.can_acquire = AsyncMock(return_value=False)
        dispatcher.counter = mock_counter

        result = asyncio.run(dispatcher.dispatch(task_pending, "L1_guardrail"))
        assert result["status"] == "dispatched"


# ---------------------------------------------------------------------------
# T4: 批量决策
# ---------------------------------------------------------------------------

class TestBatchDecision:
    def test_dispatch_pending(self, dispatcher):
        """批量决策返回所有任务的决策"""
        tasks = [
            Task(id="t1", title="T1", status="pending",
                 assigned_by="d", assignee="zhangfei-dev"),
            Task(id="t2", title="T2", status="pending",
                 assigned_by="d", assignee=None),
            Task(id="t3", title="T3", status="pending",
                 assigned_by="d", assignee="unknown"),
        ]
        results = dispatcher.dispatch_pending(tasks)
        assert len(results) == 3
        assert results[0]["level"] == DispatchLevel.FULL_AGENT
        assert results[1]["level"] == DispatchLevel.FULL_AGENT
        assert results[1]["agent_id"] == "pangtong-fujunshi"  # fallback
        assert results[2]["level"] == DispatchLevel.ESCALATE

    def test_message_building(self, dispatcher):
        """构建给 Agent 的消息"""
        task = Task(
            id="t1", title="Build Feature", status="pending",
            assigned_by="daemon", task_type="coding",
            description="Implement X", must_haves="Tests, Docs",
        )
        msg = dispatcher._build_spawn_message(task, "zhangfei-dev", {})
        assert "Build Feature" in msg
        assert "Implement X" in msg


# ---------------------------------------------------------------------------
# E14.3: Dispatcher 错误区分（v2.8 #07.1 O3 新增）
# ---------------------------------------------------------------------------

class TestDispatcherErrorClassification:
    """E14.3: Dispatcher 捕获 AgentBusyError → 日志记录具体原因 → 路由决策"""

    def test_busy_error_reason_in_result(self, dispatcher, task_pending):
        """AgentBusyError 被捕获后，结果包含具体 busy 原因（status=skipped）"""
        mock_spawner = MagicMock()
        mock_spawner.spawn_full_agent = AsyncMock(
            side_effect=AgentBusyError("zhangfei-dev", reason="session_locked",
                                       detail={"blockers": [("session_locked", 12345)]})
        )
        dispatcher.spawner = mock_spawner

        result = asyncio.run(dispatcher.dispatch(task_pending))
        assert result["status"] == "skipped"
        assert "session" in result.get("reason", "").lower() or "locked" in result.get("reason", "").lower()

    def test_counter_busy_returns_skipped(self, dispatcher, task_pending):
        """counter_blocked → skipped（非 error）"""
        mock_spawner = MagicMock()
        mock_spawner.spawn_full_agent = AsyncMock(
            side_effect=AgentBusyError("zhangfei-dev", reason="counter_blocked")
        )
        dispatcher.spawner = mock_spawner

        result = asyncio.run(dispatcher.dispatch(task_pending))
        assert result["status"] in ("skipped", "error")
