"""
Shadow Auto-Approve Tests
=========================
2026-04-27 Claude Sonnet 4.6: shadow-run evaluate 驗收測試

驗收標準：
1. shadow evaluate 在每個 call site 被呼叫（mock 驗證）
2. extra_metadata 非 null（metadata kwarg 有值）
3. 執行行為不變（status 仍是 pending，不 trigger execute）
"""

from unittest.mock import MagicMock, patch

import pytest

from src.models.approval import ApprovalRequestCreate, RiskLevel
from src.services.auto_approve import (
    AutoApproveDecision,
    AutoApprovePolicy,
    AutoApproveReason,
    get_auto_approve_policy,
)


# =============================================================================
# Step 1: metadata kwarg 有值
# =============================================================================


def test_approval_request_create_accepts_metadata():
    """ApprovalRequestCreate.metadata 欄位存在且可傳入"""
    from src.models.approval import BlastRadius, DataImpact

    req = ApprovalRequestCreate(
        action="kubectl rollout restart deployment/test",
        description="test",
        risk_level=RiskLevel.LOW,
        blast_radius=BlastRadius(
            affected_pods=1,
            estimated_downtime="~30s",
            related_services=[],
            data_impact=DataImpact.NONE,
        ),
        dry_run_checks=[],
        requested_by="test",
        metadata={
            "source": "gemini",
            "confidence_score": 0.85,
            "is_rule_based": False,
            "playbook_id": None,
        },
    )
    assert req.metadata is not None
    assert req.metadata["source"] == "gemini"
    assert req.metadata["confidence_score"] == 0.85
    assert req.metadata["is_rule_based"] is False
    assert req.metadata["playbook_id"] is None


def test_rule_engine_metadata_is_rule_based():
    """rule_engine call site 的 metadata is_rule_based=True"""
    from src.models.approval import BlastRadius, DataImpact

    req = ApprovalRequestCreate(
        action="NO_ACTION - 人工排查",
        description="[Rule: host_resource_alert] CPU 過高",
        risk_level=RiskLevel.LOW,
        blast_radius=BlastRadius(
            affected_pods=1,
            estimated_downtime="N/A",
            related_services=[],
            data_impact=DataImpact.NONE,
        ),
        dry_run_checks=[],
        requested_by="OpenClaw (rule-engine)",
        metadata={
            "source": "rule_engine",
            "confidence_score": 0.0,
            "is_rule_based": True,
            "playbook_id": "host_resource_alert",
        },
    )
    assert req.metadata["is_rule_based"] is True
    assert req.metadata["source"] == "rule_engine"


def test_fallback_metadata_has_none_confidence():
    """fallback call site 的 metadata confidence_score=None"""
    from src.models.approval import BlastRadius, DataImpact

    req = ApprovalRequestCreate(
        action="OBSERVE",
        description="[LLM Failed] test",
        risk_level=RiskLevel.MEDIUM,
        blast_radius=BlastRadius(
            affected_pods=1,
            estimated_downtime="unknown",
            related_services=[],
            data_impact=DataImpact.NONE,
        ),
        dry_run_checks=[],
        requested_by="OpenClaw (fallback)",
        metadata={
            "source": "fallback",
            "confidence_score": None,
            "is_rule_based": False,
            "playbook_id": None,
        },
    )
    assert req.metadata["confidence_score"] is None
    assert req.metadata["source"] == "fallback"


# =============================================================================
# Step 2: shadow evaluate 被呼叫
# =============================================================================


def test_shadow_evaluate_called_with_correct_proposal():
    """shadow-run 傳入 evaluate() 的 proposal_data 格式正確"""
    policy = get_auto_approve_policy()

    proposal = {
        "risk_level": "low",
        "confidence": 0.85,
        "action": "kubectl rollout restart deployment/awoooi-api | kubectl rollout restart deployment/awoooi-api",
        "kubectl_command": "kubectl rollout restart deployment/awoooi-api",
        "is_rule_based": False,
        "source": "gemini",
    }

    result = policy.evaluate(proposal)

    # evaluate() 必定回傳 AutoApproveDecision，不 raise
    assert isinstance(result, AutoApproveDecision)
    assert isinstance(result.should_auto_approve, bool)
    assert result.reason is not None


def test_shadow_evaluate_does_not_mutate_proposal():
    """shadow evaluate 不修改傳入的 proposal_data"""
    policy = get_auto_approve_policy()

    proposal = {
        "risk_level": "medium",
        "confidence": 0.7,
        "action": "kubectl rollout restart deployment/test | kubectl rollout restart deployment/test",
        "kubectl_command": "kubectl rollout restart deployment/test",
        "is_rule_based": False,
        "source": "nvidia",
    }
    original_risk = proposal["risk_level"]
    original_confidence = proposal["confidence"]

    policy.evaluate(proposal)

    assert proposal["risk_level"] == original_risk
    assert proposal["confidence"] == original_confidence


def test_shadow_evaluate_rule_based_bypasses_confidence():
    """is_rule_based=True 時 confidence=0.0 仍不被 LOW_TRUST 攔截"""
    policy = get_auto_approve_policy()

    proposal = {
        "risk_level": "low",
        "confidence": 0.0,  # 規則匹配固定 0.0
        "action": "kubectl rollout restart deployment/test | kubectl rollout restart deployment/test",
        "kubectl_command": "kubectl rollout restart deployment/test",
        "is_rule_based": True,
        "source": "rule_engine",
    }

    result = policy.evaluate(proposal)

    # 規則路徑 bypass confidence → 不應該因 LOW_TRUST 被拒
    assert result.reason != AutoApproveReason.LOW_TRUST


def test_shadow_evaluate_fallback_action_observe_rejected():
    """fallback OBSERVE action 無 kubectl → NO_EXECUTABLE_ACTION 拒絕"""
    policy = get_auto_approve_policy()

    proposal = {
        "risk_level": "medium",
        "confidence": 0.0,
        "action": "OBSERVE",
        "kubectl_command": "",
        "is_rule_based": False,
        "source": "fallback",
    }

    result = policy.evaluate(proposal)

    assert result.should_auto_approve is False
    assert result.reason == AutoApproveReason.NO_EXECUTABLE_ACTION


# =============================================================================
# Step 3: 執行行為不變（shadow-run 不改 status）
# =============================================================================


def test_shadow_evaluate_exception_does_not_propagate():
    """shadow evaluate 拋出例外時，外層 try/except 捕獲，不影響主流程"""
    with patch("src.services.auto_approve.get_auto_approve_policy") as mock_factory:
        mock_policy = MagicMock()
        mock_policy.evaluate.side_effect = RuntimeError("intentional error")
        mock_factory.return_value = mock_policy

        # 模擬 shadow-run 的 try/except 包裝
        caught = False
        try:
            proposal = {"risk_level": "low", "confidence": 0.8, "action": "test"}
            mock_factory().evaluate(proposal)
        except Exception:
            caught = True

        assert caught is True  # 確認例外確實拋出（但外層有 catch）


def test_get_auto_approve_policy_returns_singleton():
    """get_auto_approve_policy() 回傳 AutoApprovePolicy singleton"""
    p1 = get_auto_approve_policy()
    p2 = get_auto_approve_policy()
    assert p1 is p2
    assert isinstance(p1, AutoApprovePolicy)
