"""
DecisionManager._ssh_execute docker prune 路由測試
=================================================
ADR-068 飛輪 — disk full SOP（2026-05-02 ogt + Claude Sonnet 4.6）

驗證 LLM 提案 action 含 "docker prune" 時，會路由到 ssh_provider 的
ssh_docker_prune 工具，並帶上 trust_score=0.85 + host=instance label。
"""

from __future__ import annotations

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

import pytest

from src.plugins.mcp.interfaces import MCPToolResult
from src.services.decision_manager import DecisionManager


def _fake_incident(host: str = "192.168.0.110") -> SimpleNamespace:
    """最小可用 Incident stub — 只有 _ssh_execute 用到的欄位"""
    signal = SimpleNamespace(labels={"instance": host})
    return SimpleNamespace(
        incident_id="INC-TEST-PRUNE",
        signals=[signal],
    )


def _fake_token() -> SimpleNamespace:
    """最小可用 DecisionToken stub"""
    return SimpleNamespace(
        state=None,
        proposal_data={},
        error=None,
    )


@pytest.fixture
def manager(monkeypatch):
    """避免實例化重型依賴（openclaw / redis / knowledge_service）"""
    # get_knowledge_service 在 __init__ 內 import，patch 原始 module
    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()
    # 用 AsyncMock 攔截 SSH execute、token 存檔、telegram push
    mgr._ssh = MagicMock()
    mgr._ssh.execute = AsyncMock(
        return_value=MCPToolResult(success=True, execution_id="exec-1", output={"stdout": "pruned"})
    )
    mgr._save_token = AsyncMock()
    monkeypatch.setattr(
        "src.services.decision_manager._fire_and_forget",
        lambda *a, **k: None,
    )
    # _fire_and_forget 已經 patch 為 no-op，但這兩個函式被「呼叫」會建出 coroutine
    # 改用同步 stub 直接回 None 避免 unawaited coroutine warning
    monkeypatch.setattr(
        "src.services.decision_manager._push_decision_to_telegram",
        lambda *a, **k: None,
    )
    monkeypatch.setattr(
        "src.services.decision_manager._push_auto_repair_result",
        lambda *a, **k: None,
    )
    monkeypatch.setenv("SSH_MCP_ALLOWED_HOSTS", "192.168.0.110,192.168.0.188")
    return mgr


class TestDockerPruneActionRouting:
    """LLM action 字串 → ssh_docker_prune 工具路由"""

    @pytest.mark.asyncio
    async def test_ssh_docker_prune_action_routes_to_tool(self, manager):
        incident = _fake_incident()
        token = _fake_token()

        await manager._ssh_execute(
            incident=incident,
            token=token,
            action="ssh 192.168.0.110 'docker image prune -a -f && docker volume prune -f'",
            target="root",
        )

        manager._ssh.execute.assert_awaited_once()
        call = manager._ssh.execute.call_args
        assert call.kwargs["tool_name"] == "ssh_docker_prune", (
            f"expected ssh_docker_prune, got {call.kwargs.get('tool_name')}"
        )
        params = call.kwargs["parameters"]
        assert params["host"] == "192.168.0.110"
        # Group B 必須帶 trust_score >= 0.8
        assert params.get("trust_score", 0) >= 0.8

    @pytest.mark.asyncio
    async def test_short_form_docker_prune_routes(self, manager):
        """簡寫格式（無 ssh prefix 但有 docker prune 關鍵字）也應路由"""
        # 注意：必須以 ssh 開頭才命中現有路由家族；非 ssh 開頭走別的分支
        incident = _fake_incident()
        token = _fake_token()
        await manager._ssh_execute(
            incident=incident,
            token=token,
            action="ssh wooo@192.168.0.110 docker prune -a",
            target="docker",
        )
        assert manager._ssh.execute.call_args.kwargs["tool_name"] == "ssh_docker_prune"

    @pytest.mark.asyncio
    async def test_docker_restart_still_routes_to_docker_restart(self, manager):
        """加新分支不能誤傷既有 docker restart 路由"""
        incident = _fake_incident()
        token = _fake_token()
        await manager._ssh_execute(
            incident=incident,
            token=token,
            action="ssh 192.168.0.110 docker restart awoooi-api",
            target="awoooi-api",
        )
        # Codex 已把 _tool 改為 SSH MCP 全名（之前 short name 與 ssh_provider 不對齊）
        assert manager._ssh.execute.call_args.kwargs["tool_name"] == "ssh_docker_restart"

    @pytest.mark.asyncio
    async def test_diagnose_still_routes_to_ssh_diagnose(self, manager):
        """加新分支不能誤傷 diagnose 路由"""
        incident = _fake_incident()
        token = _fake_token()
        await manager._ssh_execute(
            incident=incident,
            token=token,
            action="ssh 192.168.0.110 'df -h && free -h'",
            target="unknown",
        )
        assert manager._ssh.execute.call_args.kwargs["tool_name"] == "ssh_diagnose"

    @pytest.mark.asyncio
    async def test_failed_diagnose_does_not_emit_auto_repair_failed(self, manager, monkeypatch):
        """ssh_diagnose 是只讀診斷，失敗時不可標成自動修復失敗。"""
        incident = _fake_incident()
        token = _fake_token()
        auto_result_calls = []

        manager._ssh.execute = AsyncMock(
            return_value=MCPToolResult(
                success=False,
                execution_id="exec-fail",
                error="diagnosis connection failed",
            )
        )
        monkeypatch.setattr(
            "src.services.decision_manager._push_auto_repair_result",
            lambda *args, **kwargs: auto_result_calls.append((args, kwargs)),
        )
        monkeypatch.setattr(
            "src.services.decision_manager._escalate_decision_auto_repair_unavailable",
            lambda *args, **kwargs: None,
        )

        await manager._ssh_execute(
            incident=incident,
            token=token,
            action="ssh 192.168.0.110 'df -h && free -h'",
            target="unknown",
        )

        assert manager._ssh.execute.call_args.kwargs["tool_name"] == "ssh_diagnose"
        assert token.proposal_data["automation_state"] == "diagnosis_failed_manual_required"
        assert auto_result_calls == []
