"""
B2: dispatch_llm_action 單元測試
==================================
2026-04-27 Claude Sonnet 4.6: B2 — LLM 動態 MCP 規格派發閘控

覆蓋：
1. critical risk 被拒
2. high risk 無 confirmed → 拒（附 nonce）
3. high risk + confirmed=True → 允許
4. low / medium → 直接允許
5. mcp_tool 不在 registry → 拒
6. params 模板渲染（{labels.instance} / {context.incident_id}）
7. 渲染失敗時不 crash

🔴 遵循「禁止 Mock 測試鐵律」: 不 mock registry，使用真實 YAML 或
   stub registry patch（僅 _load_llm_tool_registry 以 monkeypatch 替換，
   原因：solver_agent 有 prometheus metric 依賴，在 pure unit test 環境無法
   正常 import；stub 清單來自真實 YAML 中已知 action 名稱）。
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

import pytest

from src.services.callback_dispatcher import (
    dispatch_llm_action,
    _render_llm_params,
)


# =============================================================================
# Fixture: stub RecommendedAction（避免 import solver_agent metrics 依賴）
# =============================================================================

@dataclass
class _StubAction:
    """最小化 RecommendedAction stub，欄位與 protocol.py 完全對齊。"""
    name: str
    label: str
    emoji: str
    mcp_provider: str
    mcp_tool: str
    params: dict[str, str]
    risk: Literal["low", "medium", "high", "critical"]
    reasoning: str = ""


# =============================================================================
# Fixture: stub registry（monkeypatch _load_llm_tool_registry）
# =============================================================================

_STUB_REGISTRY: dict[str, dict] = {
    "check_pod_logs": {
        "provider": "k8s",
        "tool": "check_pod_logs",
        "risk": "low",
        "label": "查 Pod 日誌",
        "emoji": "📋",
    },
    "restart_deployment": {
        "provider": "k8s",
        "tool": "restart_deployment",
        "risk": "medium",
        "label": "重啟 Deployment",
        "emoji": "🔄",
    },
    "drain_node": {
        "provider": "k8s",
        "tool": "drain_node",
        "risk": "high",
        "label": "排空節點",
        "emoji": "⚠️",
    },
}


@pytest.fixture(autouse=True)
def _patch_registry(monkeypatch):
    """將 _load_llm_tool_registry 替換為 stub，隔離 prometheus metric 依賴。"""
    import src.services.callback_dispatcher as mod
    monkeypatch.setattr(mod, "_load_llm_tool_registry", lambda: _STUB_REGISTRY)


# =============================================================================
# 測試 1: critical risk 直接拒絕
# =============================================================================

def test_critical_risk_rejected():
    action = _StubAction(
        name="nuke_cluster",
        label="核爆",
        emoji="💣",
        mcp_provider="k8s",
        mcp_tool="delete_all_pods",
        params={},
        risk="critical",
    )
    result = dispatch_llm_action(action, {"incident_id": "INC-001"})
    assert result["ok"] is False
    assert result["reason"] == "critical_action_rejected"


# =============================================================================
# 測試 2: high risk 無 confirmed → 拒，附 nonce
# =============================================================================

def test_high_risk_no_confirmed_rejected_with_nonce():
    action = _StubAction(
        name="drain_node",
        label="排空節點",
        emoji="⚠️",
        mcp_provider="k8s",
        mcp_tool="drain_node",
        params={},
        risk="high",
    )
    result = dispatch_llm_action(action, {"incident_id": "INC-002"})
    assert result["ok"] is False
    assert result["reason"] == "high_risk_requires_confirmation"
    assert "nonce" in result
    assert isinstance(result["nonce"], str)
    assert len(result["nonce"]) > 0


# =============================================================================
# 測試 3: high risk + confirmed=True → 允許
# =============================================================================

def test_high_risk_with_confirmed_allowed():
    action = _StubAction(
        name="drain_node",
        label="排空節點",
        emoji="⚠️",
        mcp_provider="k8s",
        mcp_tool="drain_node",
        params={"node": "worker-1"},
        risk="high",
    )
    result = dispatch_llm_action(action, {"incident_id": "INC-003", "confirmed": True})
    assert result["ok"] is True
    assert result["mcp_tool"] == "drain_node"
    assert result["button_source"] == "llm"
    # high risk 允許後也應有 nonce
    assert result["nonce"] is not None


# =============================================================================
# 測試 4a: low risk 直接允許，無 nonce
# =============================================================================

def test_low_risk_allowed_no_nonce():
    action = _StubAction(
        name="check_pod_logs",
        label="查 Pod 日誌",
        emoji="📋",
        mcp_provider="k8s",
        mcp_tool="check_pod_logs",
        params={},
        risk="low",
    )
    result = dispatch_llm_action(action, {"incident_id": "INC-004"})
    assert result["ok"] is True
    assert result["nonce"] is None
    assert result["button_source"] == "llm"


# =============================================================================
# 測試 4b: medium risk 直接允許，附 nonce
# =============================================================================

def test_medium_risk_allowed_with_nonce():
    action = _StubAction(
        name="restart_deployment",
        label="重啟 Deployment",
        emoji="🔄",
        mcp_provider="k8s",
        mcp_tool="restart_deployment",
        params={},
        risk="medium",
    )
    result = dispatch_llm_action(action, {"incident_id": "INC-005"})
    assert result["ok"] is True
    assert result["nonce"] is not None
    assert result["risk"] == "medium"


# =============================================================================
# 測試 5: mcp_tool 不在 registry → 拒
# =============================================================================

def test_tool_not_in_registry_rejected():
    action = _StubAction(
        name="mystery_action",
        label="神秘動作",
        emoji="❓",
        mcp_provider="k8s",
        mcp_tool="non_existent_tool",
        params={},
        risk="low",
    )
    result = dispatch_llm_action(action, {"incident_id": "INC-006"})
    assert result["ok"] is False
    assert result["reason"] == "tool_not_in_registry"


# =============================================================================
# 測試 6: params 模板渲染
# =============================================================================

def test_params_template_rendering():
    """{labels.instance} 和 {context.incident_id} 都應正確渲染。"""
    action = _StubAction(
        name="check_pod_logs",
        label="查 Pod 日誌",
        emoji="📋",
        mcp_provider="k8s",
        mcp_tool="check_pod_logs",
        params={
            "host": "{labels.instance}",
            "namespace": "{labels.namespace}",
            "incident": "{context.incident_id}",
            "raw_id": "{incident_id}",
        },
        risk="low",
    )
    context = {
        "incident_id": "INC-007",
        "labels": {
            "instance": "192.168.0.110",
            "namespace": "production",
        },
    }
    result = dispatch_llm_action(action, context)
    assert result["ok"] is True
    assert result["params"]["host"] == "192.168.0.110"
    assert result["params"]["namespace"] == "production"
    assert result["params"]["incident"] == "INC-007"
    assert result["params"]["raw_id"] == "INC-007"


# =============================================================================
# 測試 7: 渲染失敗時不 crash，保留原始字串
# =============================================================================

def test_params_render_failure_keeps_original():
    """找不到的 key → 保留 {xxx} 原始字串，不 crash。"""
    action = _StubAction(
        name="check_pod_logs",
        label="查 Pod 日誌",
        emoji="📋",
        mcp_provider="k8s",
        mcp_tool="check_pod_logs",
        params={
            "host": "{labels.nonexistent_key}",
            "static": "no_template",
        },
        risk="low",
    )
    result = dispatch_llm_action(action, {"incident_id": "INC-008", "labels": {}})
    assert result["ok"] is True
    # 找不到的 key 保留原始模板字串
    assert result["params"]["host"] == "{labels.nonexistent_key}"
    # 靜態值不變
    assert result["params"]["static"] == "no_template"


# =============================================================================
# 測試 8: _render_llm_params 單元測試（直接測渲染函數）
# =============================================================================

def test_render_llm_params_direct():
    params = {
        "a": "{labels.zone}",
        "b": "{context.user_id}",
        "c": "literal",
        "d": "{labels.missing}",
    }
    context = {
        "labels": {"zone": "ap-east-1"},
        "user_id": "u42",
    }
    rendered = _render_llm_params(params, context)
    assert rendered["a"] == "ap-east-1"
    assert rendered["b"] == "u42"
    assert rendered["c"] == "literal"
    assert rendered["d"] == "{labels.missing}"  # 找不到 → 原始字串
