"""
test_consensus_integration.py — P2.4 12-Agent Consensus 整合測試
================================================================
測試覆蓋:
1. ENABLE_12AGENT_CONSENSUS=False → 不走 Consensus（走原有雙軌路徑）
2. ENABLE_12AGENT_CONSENSUS=True + P0 → Consensus 被呼叫
3. ENABLE_12AGENT_CONSENSUS=True + P2（非 P0/P1）→ 不走 Consensus
4. Consensus 共識分數 ≥0.6 → 回傳 READY token，risk_level="medium"
5. Consensus 拋例外 → fallback 到 expert_analyze，不阻斷主路由

測試類型: unit（mock ConsensusEngine，lazy import 透過 patch consensus_engine 模組）

2026-04-26 P2.4 by Claude — 12-Agent Consensus 整合測試
"""

from __future__ import annotations

from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.models.incident import Incident, Severity, Signal
from src.services.consensus_engine import ConsensusResult


# =============================================================================
# Fixtures
# =============================================================================


def _make_signal(alert_name: str = "HighCPUUsage") -> Signal:
    return Signal(
        alert_name=alert_name,
        severity=Severity.P0,
        source="prometheus",
        fired_at=datetime.now(timezone.utc),
    )


def _make_incident(severity: Severity = Severity.P0) -> Incident:
    return Incident(
        severity=severity,
        signals=[_make_signal()],
        affected_services=["api"],
    )


def _make_consensus_result(consensus_score: float = 0.75) -> ConsensusResult:
    from src.services.consensus_engine import AgentOpinion, AgentType

    opinion = AgentOpinion(
        agent_type=AgentType.SRE,
        action="重新啟動服務",
        reasoning="SRE 分析: 需要重啟",
        confidence=0.8,
        risk_assessment="medium",
        kubectl_command="kubectl rollout restart deployment/api",
        priority=8,
    )
    return ConsensusResult(
        consensus_id="CON-TEST-001",
        incident_id="INC-TEST-001",
        opinions=[opinion],
        consensus_score=consensus_score,
        recommended_action="重新啟動服務",
        recommended_kubectl="kubectl rollout restart deployment/api",
        final_reasoning="整合意見: 重啟",
        risk_level="medium" if consensus_score >= 0.6 else "critical",
        dissenting_opinions=[],
    )


def _make_dm_with_mocks():
    """建立帶有必要 mock 的 DecisionManager（不觸發 __init__）"""
    from src.services.decision_manager import DecisionManager

    dm = DecisionManager.__new__(DecisionManager)
    dm._save_token = AsyncMock()
    dm._find_existing_token = AsyncMock(return_value=None)
    return dm


# =============================================================================
# Test 1: ENABLE_12AGENT_CONSENSUS=False → 不走 Consensus
# =============================================================================


@pytest.mark.asyncio
async def test_consensus_disabled_skips_consensus_engine():
    """flag=False 時，get_or_create_decision_with_consensus 應直接走 get_or_create_decision"""
    incident = _make_incident(Severity.P0)

    with patch("src.services.decision_manager.settings") as mock_settings:
        mock_settings.ENABLE_12AGENT_CONSENSUS = False

        dm = _make_dm_with_mocks()
        mock_token = MagicMock()
        dm.get_or_create_decision = AsyncMock(return_value=mock_token)

        result = await dm.get_or_create_decision_with_consensus(
            incident=incident, timeout_sec=30.0
        )

    dm.get_or_create_decision.assert_awaited_once_with(incident, 30.0)
    assert result is mock_token


# =============================================================================
# Test 2: ENABLE_12AGENT_CONSENSUS=True + P0 → Consensus 被呼叫
# =============================================================================


@pytest.mark.asyncio
async def test_consensus_enabled_p0_calls_consensus_engine():
    """flag=True + P0 事件 → ConsensusEngine.run_consensus 必須被呼叫"""
    from src.services.decision_manager import DecisionState

    incident = _make_incident(Severity.P0)
    consensus_result = _make_consensus_result(consensus_score=0.75)

    mock_consensus_engine = AsyncMock()
    mock_consensus_engine.run_consensus = AsyncMock(return_value=consensus_result)

    with (
        patch("src.services.decision_manager.settings") as mock_settings,
        # decision_manager 用 lazy import：from src.services.consensus_engine import get_consensus_engine
        # 所以要 patch consensus_engine 模組本身的 singleton
        patch(
            "src.services.consensus_engine._consensus_engine",
            new=mock_consensus_engine,
        ),
        patch("src.services.consensus_engine.get_redis") as mock_redis_factory,
    ):
        mock_settings.ENABLE_12AGENT_CONSENSUS = True

        mock_redis = AsyncMock()
        mock_redis.get = AsyncMock(return_value=None)
        mock_redis.set = AsyncMock(return_value=True)
        mock_redis_factory.return_value = mock_redis

        dm = _make_dm_with_mocks()

        token = await dm.get_or_create_decision_with_consensus(
            incident=incident, timeout_sec=30.0
        )

    mock_consensus_engine.run_consensus.assert_awaited_once()
    assert token.state == DecisionState.READY
    assert token.proposal_data["source"] == "consensus_engine"
    assert token.proposal_data["consensus_score"] == 0.75


# =============================================================================
# Test 3: ENABLE_12AGENT_CONSENSUS=True + P2 → 不走 Consensus
# =============================================================================


@pytest.mark.asyncio
async def test_consensus_enabled_p2_skips_consensus():
    """flag=True 但 P2 事件（非 P0/P1）→ 走 get_or_create_decision，不呼叫 Consensus"""
    incident = _make_incident(Severity.P2)

    mock_consensus_engine = AsyncMock()
    mock_consensus_engine.run_consensus = AsyncMock()

    with (
        patch("src.services.decision_manager.settings") as mock_settings,
        patch(
            "src.services.consensus_engine._consensus_engine",
            new=mock_consensus_engine,
        ),
    ):
        mock_settings.ENABLE_12AGENT_CONSENSUS = True

        dm = _make_dm_with_mocks()
        mock_token = MagicMock()
        dm.get_or_create_decision = AsyncMock(return_value=mock_token)

        result = await dm.get_or_create_decision_with_consensus(
            incident=incident, timeout_sec=30.0
        )

    dm.get_or_create_decision.assert_awaited_once()
    mock_consensus_engine.run_consensus.assert_not_awaited()
    assert result is mock_token


# =============================================================================
# Test 4: Consensus 共識分數 ≥0.6 → token.state == READY，risk_level="medium"
# =============================================================================


@pytest.mark.asyncio
async def test_high_consensus_score_results_in_ready_token():
    """共識分數 0.8（≥0.6 閾值）→ token 狀態應為 READY，risk_level="medium" """
    from src.services.decision_manager import DecisionState

    incident = _make_incident(Severity.P1)
    consensus_result = _make_consensus_result(consensus_score=0.80)

    mock_consensus_engine = AsyncMock()
    mock_consensus_engine.run_consensus = AsyncMock(return_value=consensus_result)

    with (
        patch("src.services.decision_manager.settings") as mock_settings,
        patch(
            "src.services.consensus_engine._consensus_engine",
            new=mock_consensus_engine,
        ),
        patch("src.services.consensus_engine.get_redis") as mock_redis_factory,
    ):
        mock_settings.ENABLE_12AGENT_CONSENSUS = True

        mock_redis = AsyncMock()
        mock_redis.get = AsyncMock(return_value=None)
        mock_redis.set = AsyncMock(return_value=True)
        mock_redis_factory.return_value = mock_redis

        dm = _make_dm_with_mocks()

        token = await dm.get_or_create_decision_with_consensus(
            incident=incident, timeout_sec=30.0
        )

    assert token.state == DecisionState.READY
    assert token.proposal_data["consensus_score"] == 0.80
    assert token.proposal_data["risk_level"] == "medium"


# =============================================================================
# Test 5: Consensus 拋例外 → fallback 到 expert_analyze，不阻斷主路由
# =============================================================================


@pytest.mark.asyncio
async def test_consensus_exception_falls_back_to_expert():
    """ConsensusEngine.run_consensus 拋出例外 → fallback 到 expert_analyze，token 仍為 READY"""
    from src.services.decision_manager import DecisionState

    incident = _make_incident(Severity.P0)

    mock_consensus_engine = AsyncMock()
    mock_consensus_engine.run_consensus = AsyncMock(
        side_effect=RuntimeError("Ollama 111 offline")
    )

    with (
        patch("src.services.decision_manager.settings") as mock_settings,
        patch(
            "src.services.consensus_engine._consensus_engine",
            new=mock_consensus_engine,
        ),
        patch("src.services.decision_manager.expert_analyze") as mock_expert,
    ):
        mock_settings.ENABLE_12AGENT_CONSENSUS = True
        mock_expert.return_value = {
            "source": "expert_system",
            "action": "fallback_action",
            "confidence": 0.5,
        }

        dm = _make_dm_with_mocks()

        token = await dm.get_or_create_decision_with_consensus(
            incident=incident, timeout_sec=30.0
        )

    # Consensus 失敗後應 fallback 到 expert，token 仍為 READY（不阻斷）
    assert token.state == DecisionState.READY
    assert token.error is not None  # 錯誤訊息被記錄
    mock_expert.assert_called_once_with(incident)
