"""
DecisionManager._auto_execute bare_metal × kubectl 守衛測試
=========================================================
2026-05-02 ogt + Claude Sonnet 4.6:

當 alert label `host_type=bare_metal` 而 LLM 提案的 action 是 kubectl 類，
代表 LLM 弄錯責任域（host 層問題不該動 K8s deployment），守衛應立刻擋下、
降級人工，並透過 emergency_escalation 留痕，不執行 kubectl 動作。

回歸場景：HostHighCpuLoad on 192.168.0.110（Sentry/ClickHouse 燒 CPU），
LLM 亂提「kubectl rollout restart awoooi-api」，要被擋下。
"""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.services.decision_manager import DecisionManager, DecisionState


def _fake_incident(host_type: str = "bare_metal", alertname: str = "HostHighCpuLoad") -> SimpleNamespace:
    signal = SimpleNamespace(labels={
        "host_type": host_type,
        "alertname": alertname,
        "instance": "192.168.0.110:9100",
    })
    return SimpleNamespace(
        incident_id="INC-TEST-GUARD",
        signals=[signal],
        affected_services=["awoooi-api"],
    )


def _fake_token(action: str = "kubectl rollout restart deployment awoooi-api") -> SimpleNamespace:
    return SimpleNamespace(
        state=None,
        proposal_data={"kubectl_command": action},
        error=None,
    )


@pytest.fixture
def manager(monkeypatch):
    with patch("src.services.decision_manager.get_openclaw"), \
         patch("src.services.knowledge_service.get_knowledge_service"), \
         patch("src.plugins.mcp.providers.k8s_provider.K8sProvider"), \
         patch("src.plugins.mcp.providers.ssh_provider.SSHProvider"):
        mgr = DecisionManager()
    mgr._save_token = AsyncMock()
    monkeypatch.setattr("src.services.decision_manager._fire_and_forget", lambda *a, **k: None)
    monkeypatch.setattr(
        "src.services.decision_manager._push_decision_to_telegram",
        lambda *a, **k: None,
    )
    monkeypatch.setattr(
        "src.services.decision_manager._escalate_decision_auto_repair_unavailable",
        lambda **k: None,
    )
    return mgr


class TestBareMetalKubectlGuard:
    @pytest.mark.asyncio
    async def test_bare_metal_kubectl_action_is_blocked(self, manager):
        incident = _fake_incident(host_type="bare_metal")
        token = _fake_token("kubectl rollout restart deployment awoooi-api")
        await manager._auto_execute(incident, token)

        assert token.state == DecisionState.READY
        assert token.proposal_data["auto_executed"] is False
        assert "host_type=bare_metal" in token.proposal_data["blocked_reason"]
        assert "kubectl" in token.proposal_data["blocked_reason"]

    @pytest.mark.asyncio
    async def test_bare_metal_kubectl_get_also_blocked(self, manager):
        """kubectl get 也是 K8s 域，host_type=bare_metal 一樣不該走"""
        incident = _fake_incident(host_type="bare_metal")
        token = _fake_token("kubectl get pods -n awoooi-prod")
        await manager._auto_execute(incident, token)
        assert token.proposal_data["auto_executed"] is False

    @pytest.mark.asyncio
    async def test_bare_metal_ssh_action_is_NOT_blocked_here(self, manager, monkeypatch):
        """bare_metal + ssh action 不該被本守衛攔下（守衛只擋 kubectl）

        後續 YAML 規則或 SSH 路由會處理；本 test 確認沒誤殺 SSH path。
        實際執行會在守衛後繼續走 YAML 規則 / NEMOTRON 路由，這裡用 mock 攔住確保
        守衛沒提早 return。
        """
        incident = _fake_incident(host_type="bare_metal")
        token = _fake_token("ssh 192.168.0.110 'ps aux --sort=-%cpu'")

        # 提早 mock 後續流程的入口，讓我們可確認沒被守衛 short-circuit
        monkeypatch.setattr(
            "src.services.decision_manager.parse_kubectl_action",
            lambda *a, **k: SimpleNamespace(operation=None, target=None),
        )
        # 守衛若沒擋下，會繼續執行 → token 不會立刻被設成 READY+blocked
        try:
            await manager._auto_execute(incident, token)
        except Exception:
            pass  # 後續路徑可能因 mock 不全而 raise，重點是沒被守衛 early-return
        # blocked_reason 不應該是「host_type=bare_metal」(那是守衛 message)
        br = token.proposal_data.get("blocked_reason", "")
        assert "host_type=bare_metal" not in br

    @pytest.mark.asyncio
    async def test_k8s_alert_kubectl_NOT_blocked(self, manager, monkeypatch):
        """non-bare_metal alert（如 K8s pod 告警）+ kubectl action → 守衛不該攔"""
        incident = _fake_incident(host_type="kubernetes", alertname="KubePodCrashLooping")
        token = _fake_token("kubectl rollout restart deployment awoooi-api")

        monkeypatch.setattr(
            "src.services.decision_manager.parse_kubectl_action",
            lambda *a, **k: SimpleNamespace(operation=None, target=None),
        )
        try:
            await manager._auto_execute(incident, token)
        except Exception:
            pass
        br = token.proposal_data.get("blocked_reason", "")
        # 不該是 bare_metal 守衛擋的（其他守衛擋是另回事）
        assert "host_type=bare_metal" not in br

    @pytest.mark.asyncio
    async def test_no_host_type_label_NOT_blocked(self, manager, monkeypatch):
        """沒 host_type label（舊規則）+ kubectl action → 守衛不該攔（保留現狀）"""
        signal = SimpleNamespace(labels={"alertname": "PodCrashLoop"})
        incident = SimpleNamespace(
            incident_id="INC-NO-HOST-TYPE",
            signals=[signal],
            affected_services=["awoooi-api"],
        )
        token = _fake_token("kubectl rollout restart deployment awoooi-api")

        monkeypatch.setattr(
            "src.services.decision_manager.parse_kubectl_action",
            lambda *a, **k: SimpleNamespace(operation=None, target=None),
        )
        try:
            await manager._auto_execute(incident, token)
        except Exception:
            pass
        br = token.proposal_data.get("blocked_reason", "")
        assert "host_type=bare_metal" not in br
