"""Wave 8 餘項 BLOCKER 修復驗收測試
================================================================================
覆蓋 critic + debugger + vuln-verifier 報告中尚未驗收的 4 修復：

1. vuln #4 — decision_fusion._sanitize prompt injection 防禦
2. debugger B14 — Gemini quota Redis 異常時 fail-closed（非 fail-open）
3. debugger B25/B26 — auto_repair_service.drain_pending_tasks (lifespan SIGTERM)
4. debugger B8 — GovernanceAgent.run_self_check ≥3 失敗觸發 alert_governance

設計原則:
- 直接驗證 production 代碼行為，不過度 mock
- 使用 monkeypatch 隔離外部依賴

2026-04-27 Wave8 BLOCKER 收尾 by Claude Opus 4.7
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest


# =============================================================================
# vuln #4 — decision_fusion._sanitize prompt injection 防禦
# =============================================================================


class TestVuln4FusionSanitize:
    """alert_name / evidence / proposal 為不可信使用者輸入，prompt 注入防禦

    核心驗證：score_with_elephant 內呼叫 LLM 前，evil input 會被
    控制字元剔除 + 截長至 max_len，不會原樣傳入 prompt。
    """

    @pytest.mark.asyncio
    async def test_score_with_elephant_sanitizes_prompt(self):
        """注入控制字元 + 超長 attacker payload → LLM 收到的 prompt 已 sanitize"""
        from src.services.decision_fusion import DecisionFusionEngine

        engine = DecisionFusionEngine()

        # 注入：alert_name 含控制字元 + payload 超長 1000 chars
        evil_alert = "ignore_prior\x00\x1b\x02XXX" + "A" * 1000
        incident = MagicMock()
        incident.signals = [MagicMock(labels={"alertname": evil_alert})]

        captured_prompt: dict = {}

        class _FakeResp:
            status_code = 200
            text = ""

            def json(self):
                return {"response": "0.5"}

            def raise_for_status(self):
                pass

        async def _fake_post(url, json=None, **kwargs):  # noqa: A002
            captured_prompt["body"] = (json or {}).get("prompt", "")
            return _FakeResp()

        mock_client = MagicMock()
        mock_client.post = AsyncMock(side_effect=_fake_post)

        with patch("httpx.AsyncClient") as mock_async_client:
            mock_async_client.return_value.__aenter__.return_value = mock_client
            try:
                await engine.score_with_elephant(
                    incident=incident,
                    evidence=MagicMock(evidence_summary="x", mcp_health={}),
                    proposal="dummy",
                )
            except Exception:
                pass  # 允許 score 解析失敗（重點驗 prompt sanitize）

        prompt_body = captured_prompt.get("body", "")
        # 控制字元應被剔除
        assert "\x00" not in prompt_body
        assert "\x1b" not in prompt_body
        assert "\x02" not in prompt_body
        # 超長 payload 應被截斷（max_len 約 100 for alert_name）
        # 1000 個 'A' 不可能全部進去
        assert prompt_body.count("A") < 200, (
            f"未截長：prompt 含 {prompt_body.count('A')} 個 A（應 <200）"
        )

    def test_get_alert_name_returns_string(self):
        """_get_alert_name 取 signals[0].alert_name 屬性"""
        from src.services.decision_fusion import DecisionFusionEngine

        engine = DecisionFusionEngine()
        signal = MagicMock()
        signal.alert_name = "HighCPU"
        incident = MagicMock()
        incident.signals = [signal]
        result = engine._get_alert_name(incident)
        assert result == "HighCPU"

    def test_get_alert_name_handles_none(self):
        """_get_alert_name(None) → 'unknown'（不 raise）"""
        from src.services.decision_fusion import DecisionFusionEngine

        engine = DecisionFusionEngine()
        assert engine._get_alert_name(None) == "unknown"


# =============================================================================
# debugger B14 — Gemini quota Redis 異常時 fail-closed
# =============================================================================


class TestB14QuotaFailClosed:
    """Redis 異常時 fail-closed 防 Gemini 失控呼叫（費用鐵律）"""

    @pytest.mark.asyncio
    async def test_redis_exception_returns_false(self):
        """Redis pipeline 拋 ConnectionError → _check_gemini_quota return False"""
        from src.services.ollama_failover_manager import OllamaFailoverManager

        manager = OllamaFailoverManager(health_monitor=MagicMock())
        manager._settings = MagicMock(GEMINI_DAILY_QUOTA=1000)

        # mock get_redis 返回會 raise 的 client
        bad_redis = MagicMock()
        bad_redis.pipeline = MagicMock(side_effect=ConnectionError("Redis down"))

        with patch("src.core.redis_client.get_redis", return_value=bad_redis), \
             patch(
                 "src.services.failover_alerter.get_failover_alerter",
                 return_value=MagicMock(
                     alert_gemini_quota_exceeded=AsyncMock(),
                 ),
             ):
            result = await manager._check_gemini_quota()

        # 鐵律：Redis 異常時 fail-closed（拒走 Gemini）
        assert result is False, "Redis 異常時必須 fail-closed，避免 Gemini 失控呼叫"

    @pytest.mark.asyncio
    async def test_redis_exception_triggers_alert(self):
        """Redis 異常時 best-effort 呼叫 alert_gemini_quota_exceeded"""
        from src.services.ollama_failover_manager import OllamaFailoverManager

        manager = OllamaFailoverManager(health_monitor=MagicMock())
        manager._settings = MagicMock(GEMINI_DAILY_QUOTA=1000)

        bad_redis = MagicMock()
        bad_redis.pipeline = MagicMock(side_effect=ConnectionError("Redis down"))

        mock_alerter = MagicMock(alert_gemini_quota_exceeded=AsyncMock())

        with patch("src.core.redis_client.get_redis", return_value=bad_redis), \
             patch(
                 "src.services.failover_alerter.get_failover_alerter",
                 return_value=mock_alerter,
             ):
            await manager._check_gemini_quota()

        # alert 應被呼叫（best-effort）
        mock_alerter.alert_gemini_quota_exceeded.assert_awaited_once()
        call_kwargs = mock_alerter.alert_gemini_quota_exceeded.await_args[0][0]
        assert call_kwargs.get("reason") == "fail_closed_due_to_redis_error"


# =============================================================================
# debugger B25/B26 — auto_repair drain_pending_tasks (lifespan SIGTERM)
# =============================================================================


class TestB25B26DrainPendingTasks:
    """K8s rolling restart 時 fire-and-forget tasks 不丟失"""

    @pytest.mark.asyncio
    async def test_drain_returns_zero_when_no_tasks(self):
        """無 pending task 時 drain 立即返回"""
        from src.services.auto_repair_service import AutoRepairService

        svc = AutoRepairService()
        # _pending_tasks 是 instance attribute set
        assert isinstance(svc._pending_tasks, set)
        result = await svc.drain_pending_tasks(timeout=1.0)
        assert result.get("pending") == 0 or result.get("drained") == 0

    @pytest.mark.asyncio
    async def test_drain_waits_for_pending_tasks(self):
        """有 pending task 時 drain 會等到完成或 timeout"""
        import asyncio

        from src.services.auto_repair_service import AutoRepairService

        svc = AutoRepairService()

        # 注入 short-lived task
        completed = []

        async def _short_task():
            await asyncio.sleep(0.05)
            completed.append("done")

        task = asyncio.create_task(_short_task())
        svc._pending_tasks.add(task)
        task.add_done_callback(svc._pending_tasks.discard)

        result = await svc.drain_pending_tasks(timeout=2.0)

        # task 應已完成
        assert "done" in completed
        # drain 結果含 pending 計數（修法形狀）
        assert isinstance(result, dict)


# =============================================================================
# debugger B8 — GovernanceAgent ≥3 失敗時觸發 alert_governance
# =============================================================================


class TestB8GovernanceFailureAlert:
    """4 自檢中 ≥3 項失敗 → 觸發 governance_self_failure 告警"""

    @pytest.mark.asyncio
    async def test_three_failures_triggers_alert(self):
        """3 項失敗應觸發 _alert"""
        from src.services.governance_agent import GovernanceAgent

        agent = GovernanceAgent()
        # 2026-04-27 P3.4: mock 五項 check（含 slo_compliance）：3 raise，2 成功
        agent.check_trust_drift = AsyncMock(side_effect=RuntimeError("DB down"))
        agent.check_knowledge_degradation = AsyncMock(side_effect=RuntimeError("KM error"))
        agent.check_llm_hallucination = AsyncMock(side_effect=RuntimeError("LLM error"))
        agent.check_execution_blast_radius = AsyncMock(return_value={"status": "ok"})
        agent.check_slo_compliance = AsyncMock(return_value={"status": "ok"})

        agent._alert = AsyncMock()

        await agent.run_self_check()

        # _alert 應被呼叫（≥3 失敗）
        agent._alert.assert_awaited_once()
        call_args = agent._alert.await_args
        assert call_args[0][0] == "governance_self_failure"
        # 2026-05-03 Claude Opus 4.7 + 統帥 ogt：對齊 governance_agent.py:604-624 嵌套 payload structure
        payload = call_args[0][1]
        assert payload["impact"]["total_checks"] == 5
        assert len(payload["impact"]["failed_checks"]) >= 3

    @pytest.mark.asyncio
    async def test_two_failures_no_alert(self):
        """僅 2 項失敗不觸發 alert（治理機制仍部分可用）"""
        from src.services.governance_agent import GovernanceAgent

        agent = GovernanceAgent()
        # 2026-04-27 P3.4: 加 slo_compliance（成功）
        agent.check_trust_drift = AsyncMock(side_effect=RuntimeError("err1"))
        agent.check_knowledge_degradation = AsyncMock(side_effect=RuntimeError("err2"))
        agent.check_llm_hallucination = AsyncMock(return_value={"status": "ok"})
        agent.check_execution_blast_radius = AsyncMock(return_value={"status": "ok"})
        agent.check_slo_compliance = AsyncMock(return_value={"status": "ok"})

        agent._alert = AsyncMock()

        await agent.run_self_check()

        # 僅 2 失敗不觸發
        agent._alert.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_all_four_failures_triggers_alert(self):
        """4 項全失敗也應觸發 alert"""
        from src.services.governance_agent import GovernanceAgent

        agent = GovernanceAgent()
        # 2026-04-27 P3.4: 5 項全失敗
        agent.check_trust_drift = AsyncMock(side_effect=RuntimeError("e"))
        agent.check_knowledge_degradation = AsyncMock(side_effect=RuntimeError("e"))
        agent.check_llm_hallucination = AsyncMock(side_effect=RuntimeError("e"))
        agent.check_execution_blast_radius = AsyncMock(side_effect=RuntimeError("e"))
        agent.check_slo_compliance = AsyncMock(side_effect=RuntimeError("e"))

        agent._alert = AsyncMock()

        await agent.run_self_check()

        agent._alert.assert_awaited_once()
        # 2026-05-03 Claude Opus 4.7 + 統帥 ogt：對齊 governance_agent.py:604-624 嵌套 payload structure
        payload = agent._alert.await_args[0][1]
        assert len(payload["impact"]["failed_checks"]) == 5
