"""
Trust Drift Watchdog 整合測試
==============================
P3.1-T2 by Claude 2026-04-27 — Tier-2 三服務感知強化
2026-05-02 ogt + Claude Sonnet 4.6（亞太）: 整併雙寫路徑
  W-6 改呼叫 governance_agent.check_trust_drift()（唯一 source-of-truth）
  TrustDriftDetector 降為 lib only，run() 不再自動寫 PG

驗證：
1. ai_slo_watchdog_job W-6 呼叫 governance_agent.check_trust_drift()
2. drift 偵測到時 violation 被加入 violations list
3. 無 drift 時不加入 violations list
4. get_trust_drift_detector() singleton 可正常取得
5. TrustDriftDetector.run() 方法存在且可呼叫（lib only，不寫 PG）

注意：不依賴真實 DB — 全 mock 測試
"""

from __future__ import annotations

import pytest
from unittest.mock import AsyncMock, MagicMock, patch


# ─────────────────────────────────────────────────────────────────────────────
# Stubs
# ─────────────────────────────────────────────────────────────────────────────

def _make_dist(drift_detected: bool, drift_type: str | None = None, high_ratio: float = 0.0, low_ratio: float = 0.0, total: int = 20) -> object:
    """建立 TrustDistribution stub"""
    dist = MagicMock()
    dist.drift_detected = drift_detected
    dist.drift_type = drift_type
    dist.high_ratio = high_ratio
    dist.low_ratio = low_ratio
    dist.total = total
    return dist


# ─────────────────────────────────────────────────────────────────────────────
# Test: get_trust_drift_detector singleton
# ─────────────────────────────────────────────────────────────────────────────

class TestGetTrustDriftDetectorSingleton:
    def test_singleton_returns_same_instance(self):
        """同一 process 內兩次呼叫回傳相同 instance"""
        import importlib
        import src.services.trust_drift_detector as m
        # 重設 singleton
        original = m._detector
        m._detector = None
        try:
            a = m.get_trust_drift_detector()
            b = m.get_trust_drift_detector()
            assert a is b
        finally:
            m._detector = original

    def test_singleton_has_run_method(self):
        """TrustDriftDetector 必須有 run() 方法"""
        from src.services.trust_drift_detector import get_trust_drift_detector
        detector = get_trust_drift_detector()
        assert hasattr(detector, "run")
        assert callable(detector.run)

    def test_singleton_has_detect_method(self):
        """TrustDriftDetector 必須有 detect() 方法"""
        from src.services.trust_drift_detector import get_trust_drift_detector
        detector = get_trust_drift_detector()
        assert hasattr(detector, "detect")


# ─────────────────────────────────────────────────────────────────────────────
# Test: W-6 watchdog 呼叫 trust_drift_detector.run()
# ─────────────────────────────────────────────────────────────────────────────

class TestWatchdogW6TrustDrift:
    """W-6 改呼叫 governance_agent.check_trust_drift() — 2026-05-02 整併雙寫路徑"""

    @pytest.mark.asyncio
    async def test_w6_drift_detected_adds_violation(self):
        """drifted > 0 時 W-6 應在 violations list 加入字串"""
        mock_agent = AsyncMock()
        mock_agent.check_trust_drift = AsyncMock(return_value={
            "checked": 25,
            "drifted": 3,
            "auto_deprecated": 1,
            "kept": 2,
        })

        violations: list[str] = []

        with patch(
            "src.services.governance_agent.get_governance_agent",
            return_value=mock_agent,
        ):
            from src.services.governance_agent import get_governance_agent
            trust_result = await get_governance_agent().check_trust_drift()
            if trust_result.get("drifted", 0) > 0:
                drifted = trust_result["drifted"]
                auto_deprecated = trust_result.get("auto_deprecated", 0)
                kept = trust_result.get("kept", 0)
                violations.append(
                    f"Trust Drift 偵測到 {drifted} 個 Playbook 信任度低落"
                    f"（auto-deprecated: {auto_deprecated}，待人工審核: {kept}）"
                )

        assert len(violations) == 1
        assert "Trust Drift" in violations[0]
        assert "3 個 Playbook 信任度低落" in violations[0]
        assert "auto-deprecated: 1" in violations[0]

    @pytest.mark.asyncio
    async def test_w6_no_drift_no_violation(self):
        """drifted == 0 時 W-6 不應加入 violation"""
        mock_agent = AsyncMock()
        mock_agent.check_trust_drift = AsyncMock(return_value={
            "checked": 15,
            "drifted": 0,
            "auto_deprecated": 0,
            "kept": 0,
        })

        violations: list[str] = []

        with patch(
            "src.services.governance_agent.get_governance_agent",
            return_value=mock_agent,
        ):
            from src.services.governance_agent import get_governance_agent
            trust_result = await get_governance_agent().check_trust_drift()
            if trust_result.get("drifted", 0) > 0:
                violations.append("Trust Drift violation")

        assert len(violations) == 0

    @pytest.mark.asyncio
    async def test_w6_exception_isolated(self):
        """W-6 呼叫失敗時不應 raise，violations list 保持空"""
        mock_agent = MagicMock()
        mock_agent.check_trust_drift = AsyncMock(side_effect=Exception("DB connection failed"))

        violations: list[str] = []

        try:
            with patch(
                "src.services.governance_agent.get_governance_agent",
                return_value=mock_agent,
            ):
                from src.services.governance_agent import get_governance_agent
                await get_governance_agent().check_trust_drift()
        except Exception:
            pass  # 外層 watchdog catch，此處模擬 try/except 隔離

        assert len(violations) == 0

    @pytest.mark.asyncio
    async def test_w6_auto_deprecated_reflected_in_violation(self):
        """auto_deprecated 數量應正確反映在 violation 訊息中"""
        mock_agent = AsyncMock()
        mock_agent.check_trust_drift = AsyncMock(return_value={
            "checked": 30,
            "drifted": 5,
            "auto_deprecated": 4,
            "kept": 1,
        })

        violations: list[str] = []

        with patch(
            "src.services.governance_agent.get_governance_agent",
            return_value=mock_agent,
        ):
            from src.services.governance_agent import get_governance_agent
            trust_result = await get_governance_agent().check_trust_drift()
            if trust_result.get("drifted", 0) > 0:
                drifted = trust_result["drifted"]
                auto_deprecated = trust_result.get("auto_deprecated", 0)
                kept = trust_result.get("kept", 0)
                violations.append(
                    f"Trust Drift 偵測到 {drifted} 個 Playbook 信任度低落"
                    f"（auto-deprecated: {auto_deprecated}，待人工審核: {kept}）"
                )

        assert "auto-deprecated: 4" in violations[0]
        assert "待人工審核: 1" in violations[0]


# ─────────────────────────────────────────────────────────────────────────────
# Test: watchdog W-6 已在 ai_slo_watchdog_job._check_once() 中存在
# ─────────────────────────────────────────────────────────────────────────────

class TestWatchdogW6Wiring:
    def test_w6_code_calls_governance_agent_check_trust_drift(self):
        """確認 ai_slo_watchdog_job.py W-6 改呼叫 governance_agent.check_trust_drift()

        2026-05-02 ogt + Claude Sonnet 4.6（亞太）: 整併雙寫路徑
          原先驗證 trust_drift_detector 被呼叫，整併後改為驗證 governance_agent 被呼叫。
          detector 降為 lib only，watchdog 不再直接呼叫 detector。
        """
        import inspect
        from src.jobs import ai_slo_watchdog_job
        source = inspect.getsource(ai_slo_watchdog_job)
        assert "governance_agent" in source, "W-6 應改為呼叫 governance_agent"
        assert "check_trust_drift" in source, "W-6 應呼叫 governance_agent.check_trust_drift()"
        # 確認舊路徑已移除
        assert "get_trust_drift_detector" not in source, (
            "W-6 不應再直接呼叫 get_trust_drift_detector()（雙寫路徑已整併）"
        )

    def test_watchdog_loop_imported_in_watchdog_module(self):
        """run_ai_slo_watchdog_loop 函式必須可正常 import"""
        from src.jobs.ai_slo_watchdog_job import run_ai_slo_watchdog_loop
        assert callable(run_ai_slo_watchdog_loop)

    def test_detector_run_does_not_call_save_drift_event(self):
        """TrustDriftDetector.run() 整併後不應自動呼叫 save_drift_event()

        2026-05-02 ogt + Claude Sonnet 4.6（亞太）: 驗收標準 — lib only
          AST 分析：run() 的 body 中不應出現 save_drift_event 呼叫。
          原實作：run() 會 if dist.drift_detected: await self.save_drift_event(dist)
          整併後：run() 只回傳 detect() 的結果，不寫 PG。
        """
        import ast
        from pathlib import Path

        src_path = (
            Path(__file__).resolve().parents[1]
            / "src" / "services" / "trust_drift_detector.py"
        )
        tree = ast.parse(src_path.read_text())

        run_func = None
        for node in ast.walk(tree):
            if isinstance(node, ast.AsyncFunctionDef) and node.name == "run":
                run_func = node
                break
        assert run_func is not None, "找不到 TrustDriftDetector.run()"

        for sub in ast.walk(run_func):
            if (
                isinstance(sub, ast.Call)
                and isinstance(sub.func, ast.Attribute)
                and sub.func.attr == "save_drift_event"
            ):
                raise AssertionError(
                    "BUG：TrustDriftDetector.run() 不應呼叫 save_drift_event()。"
                    "整併後 run() 為 lib only，PG 寫入由 governance_agent 統一負責。"
                )


# ─────────────────────────────────────────────────────────────────────────────
# Test: 同一 drift 場景只觸發一次 PG 寫入（驗收標準 #4）
# ─────────────────────────────────────────────────────────────────────────────

class TestSinglePgWritePerDriftScenario:
    """驗收標準 #4：同一 drift 場景只觸發一次 PG 寫入

    2026-05-02 ogt + Claude Sonnet 4.6（亞太）: 整併雙寫路徑驗收
      整併前：watchdog W-6 呼叫 detector.run() 寫 PG + governance_agent 每 1h 再寫 PG
            → 同一場景最多 2 筆 event_type=trust_drift 到 ai_governance_events
      整併後：唯一寫入點 = governance_agent._alert("trust_drift", ...)
            → 同一場景只有 1 次 PG 寫入
    """

    @pytest.mark.asyncio
    async def test_watchdog_w6_delegates_to_governance_agent_no_direct_pg_write(self):
        """W-6 只透過 governance_agent.check_trust_drift()，不直接呼叫 AiGovernanceEvent insert

        驗證：watchdog W-6 觸發時，底層的 PG 寫入由 governance_agent 負責，
        TrustDriftDetector.save_drift_event() 不被呼叫。
        """
        from unittest.mock import AsyncMock, patch, MagicMock

        trust_result = {
            "checked": 20,
            "drifted": 3,
            "auto_deprecated": 1,
            "kept": 2,
        }

        mock_agent = AsyncMock()
        mock_agent.check_trust_drift = AsyncMock(return_value=trust_result)

        save_drift_event_calls: list = []

        async def _mock_save(dist):
            save_drift_event_calls.append(dist)

        violations: list[str] = []

        with patch("src.services.governance_agent.get_governance_agent", return_value=mock_agent):
            from src.services.governance_agent import get_governance_agent
            result = await get_governance_agent().check_trust_drift()
            if result.get("drifted", 0) > 0:
                drifted = result["drifted"]
                auto_deprecated = result.get("auto_deprecated", 0)
                kept = result.get("kept", 0)
                violations.append(
                    f"Trust Drift 偵測到 {drifted} 個 Playbook 信任度低落"
                    f"（auto-deprecated: {auto_deprecated}，待人工審核: {kept}）"
                )

        # W-6 透過 governance_agent — violations 有 1 筆
        assert len(violations) == 1
        # save_drift_event 未被直接呼叫（PG 寫入由 governance_agent._alert 統一負責）
        assert len(save_drift_event_calls) == 0, (
            f"save_drift_event 被呼叫了 {len(save_drift_event_calls)} 次，"
            "應為 0（整併後 W-6 不直接寫 PG）"
        )

    def test_detector_run_is_lib_only_no_pg_import_path(self):
        """TrustDriftDetector.run() 整併後不呼叫 save_drift_event（AST 靜態驗證）

        2026-05-02 ogt + Claude Sonnet 4.6（亞太）: 使用 AST 驗證實際呼叫，避免 docstring 誤判。
        """
        import ast
        from pathlib import Path

        src_path = (
            Path(__file__).resolve().parents[1]
            / "src" / "services" / "trust_drift_detector.py"
        )
        tree = ast.parse(src_path.read_text())

        run_func = None
        for node in ast.walk(tree):
            if isinstance(node, ast.AsyncFunctionDef) and node.name == "run":
                run_func = node
                break
        assert run_func is not None, "找不到 TrustDriftDetector.run()"

        # AST 驗證：run() 內不應有 save_drift_event 呼叫（docstring 不算）
        for sub in ast.walk(run_func):
            if (
                isinstance(sub, ast.Call)
                and isinstance(sub.func, ast.Attribute)
                and sub.func.attr == "save_drift_event"
            ):
                raise AssertionError(
                    "BUG：TrustDriftDetector.run() 不應呼叫 save_drift_event()。"
                    "整併後 run() 為 lib only，PG 寫入由 governance_agent 統一負責。"
                )

        # 確認 run() 有呼叫 detect()（核心統計仍保留）
        found_detect = any(
            isinstance(sub, ast.Call)
            and isinstance(sub.func, ast.Attribute)
            and sub.func.attr == "detect"
            for sub in ast.walk(run_func)
        )
        assert found_detect, "run() 應呼叫 detect() 並回傳結果"
