"""Regression guard：擋住 governance_agent.check_trust_drift 的 commit-outside-context P0 bug 復發

驗證標的：apps/api/src/services/governance_agent.py:75-171（check_trust_drift）

# 歷史背景

2026-05-02 commit dedb1208 引入 auto-deprecate 路徑時，`if auto_deprecated_ids:`
區塊縮排錯誤（縮排 8 空格 = `async with` 同層 = 區塊**外**），導致：
- session 已被 context manager 關閉並 auto-commit
- 二次 `await db.commit()` 在已關閉 session 上拋 InvalidRequestError
- 外層 try/except 吞掉錯誤
- `governance_trust_drift_auto_deprecated` log 從不出現

實際後果：DB 仍有 commit（context manager auto-commit 已落地 `status='deprecated'`），
但 log 不出現，所有依賴此 log 做 monitoring 的告警鏈會誤判系統健康。
commit b710f3f3 message 聲稱「自治路徑生效」是假象。

git diff 鐵證 (line 109)：縮排原本 8 空格在 with 外，2026-05-02 修復後改為 12 空格在 with 內。

# 並行調度教訓

vuln-verifier 與 fullstack-engineer 並行派遣時，vuln-verifier 讀取的是已被
fullstack-engineer 修改後的代碼，AST 分析得出「bug 不存在」的錯誤結論。
未來：vuln-verifier 應該在 fullstack-engineer 之**前**跑（修復前驗證 bug 真實），
或用 git show HEAD~1 比對「修復前版本」。

# 本檔角色

修復後保留為 AST regression guard：未來若 indent 退回 8 空格（`if auto_deprecated_ids:`
從 with 內被移到外層）AST 測試會立刻 fail，擋住 silent failure 復發。

2026-05-02 by ogt + Claude Opus 4.7（修正 vuln-verifier 並行誤判）
"""
from __future__ import annotations

import ast
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import structlog
from sqlalchemy.exc import InvalidRequestError

from src.services.governance_agent import GovernanceAgent

# 配置 structlog 走標準 logging（caplog 才抓得到）
structlog.configure(
    processors=[
        structlog.stdlib.add_log_level,
        structlog.processors.KeyValueRenderer(),
    ],
    wrapper_class=structlog.stdlib.BoundLogger,
    logger_factory=structlog.stdlib.LoggerFactory(),
    cache_logger_on_first_use=False,
)


# ============================================================================
# 證據 A：AST 靜態分析 — 證明 commit + log 在 with 區塊內
# ============================================================================

def test_ast_proves_commit_and_log_inside_with_block():
    """AST 證據：解析 governance_agent.py，確認 line 113 的 If（含 commit + log）
    是 AsyncWith.body 的一部分，而不是函式 body 的頂層語句。

    若 critic 主張為真，line 113 應該出現在「Function-level statements」中，
    而不是 AsyncWith.body 中。
    """
    src_path = (
        Path(__file__).resolve().parents[1]
        / "src" / "services" / "governance_agent.py"
    )
    tree = ast.parse(src_path.read_text())

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

    # 找到第一個 AsyncWith
    async_with = next(
        (s for s in func.body if isinstance(s, ast.AsyncWith)), None
    )
    assert async_with is not None

    # AsyncWith.body 內找有沒有 await db.commit() + logger.info(governance_trust_drift_auto_deprecated)
    found_commit_inside_with = False
    found_log_inside_with = False

    for sub in ast.walk(async_with):
        # await db.commit()
        if isinstance(sub, ast.Await):
            call = sub.value
            if (
                isinstance(call, ast.Call)
                and isinstance(call.func, ast.Attribute)
                and call.func.attr == "commit"
            ):
                found_commit_inside_with = True
        # logger.info("governance_trust_drift_auto_deprecated", ...)
        if (
            isinstance(sub, ast.Call)
            and isinstance(sub.func, ast.Attribute)
            and sub.func.attr == "info"
            and sub.args
            and isinstance(sub.args[0], ast.Constant)
            and sub.args[0].value == "governance_trust_drift_auto_deprecated"
        ):
            found_log_inside_with = True

    assert found_commit_inside_with, (
        "AST 證據：await db.commit() 應該出現在 AsyncWith 區塊內。"
        "若 critic 主張正確（commit 在 with 外），這裡會找不到，斷言應失敗。"
    )
    assert found_log_inside_with, (
        "AST 證據：logger.info('governance_trust_drift_auto_deprecated', ...) "
        "應該出現在 AsyncWith 區塊內。"
    )

    # 檢查函式 body（不深入子節點）—— commit + log 不該出現在頂層
    for top_stmt in func.body:
        # 直接子節點 — 不是 AsyncWith 的話，不該包含 commit/log
        if isinstance(top_stmt, ast.AsyncWith):
            continue
        for sub in ast.walk(top_stmt):
            # 不該出現 await db.commit() 在 with 外
            if isinstance(sub, ast.Await):
                call = sub.value
                if (
                    isinstance(call, ast.Call)
                    and isinstance(call.func, ast.Attribute)
                    and call.func.attr == "commit"
                ):
                    raise AssertionError(
                        f"BUG！await db.commit() 出現在函式頂層 line {sub.lineno}（with 區塊外）— "
                        "critic 主張為真，這就是 silent failure 的來源。"
                    )


# ============================================================================
# 證據 B：行為驗證 — 即使在「context-exit-closes-session」嚴格 mock 下
# log 也正常出現，證明 commit 跑在 with 內
# ============================================================================

def _make_low_trust_old_playbook(playbook_id: str = "PB-STALE-OLD"):
    rec = MagicMock()
    rec.trust_score = 0.05
    rec.playbook_id = playbook_id
    rec.status = "approved"
    rec.last_used_at = None
    rec.created_at = datetime.now(timezone.utc) - timedelta(days=45)
    return rec


class _ClosedAfterExitContext:
    """模擬真實 SQLAlchemy AsyncSession 行為：
    __aexit__ 後將 commit 替換為拋 InvalidRequestError，模擬 closed session。

    若 critic 主張為真（commit 在 with 外）：
      → 第二次 commit 會拋錯 → log 不會出現
    若 critic 主張為假（commit 在 with 內）：
      → commit 在 __aexit__ 之前就執行完 → log 正常出現

    第二次 enter（_alert 內）我們也保持替換行為（_alert 內的 commit 該拋錯，
    這是良性的 — _alert 自身有 try/except 吞掉並 log governance_pg_write_failed）。
    """

    def __init__(self, db):
        self._db = db
        self.enter_count = 0
        self.exit_count = 0

    async def __aenter__(self):
        self.enter_count += 1
        return self._db

    async def __aexit__(self, exc_type, exc, tb):
        self.exit_count += 1

        async def _raise_closed():
            raise InvalidRequestError(
                "(simulated) Session is closed; commit() called after context exit"
            )

        self._db.commit = _raise_closed
        return False


@pytest.mark.asyncio
async def test_log_appears_proves_commit_runs_inside_with(caplog, capsys):
    """行為證據：當 __aexit__ 後 commit 會拋錯時，log 仍然出現
    → 證明 commit 是在 with 內就跑完了，不在 with 外（與 critic 主張相反）
    """
    caplog.set_level(logging.INFO)

    stale = _make_low_trust_old_playbook("PB-STALE-OLD")

    mock_result = MagicMock()
    mock_result.scalars.return_value.all.return_value = [stale]

    mock_db = AsyncMock()
    mock_db.execute = AsyncMock(return_value=mock_result)
    mock_db.commit = AsyncMock()  # 初始 commit OK，__aexit__ 後會被替換成拋錯
    commit_before_exit = mock_db.commit

    alerter = AsyncMock()
    alerter.alert_governance = AsyncMock()
    agent = GovernanceAgent(alerter=alerter)

    ctx = _ClosedAfterExitContext(mock_db)

    raised = None
    with patch(
        "src.services.governance_agent.get_db_context",
        return_value=ctx,
    ):
        try:
            result = await agent.check_trust_drift()
        except InvalidRequestError as e:
            raised = e
            result = None

    # 1. with 區塊有正確 enter/exit
    assert ctx.enter_count >= 1
    assert ctx.exit_count >= 1

    # 2. mutation 已發生
    assert stale.status == "deprecated"

    # 3. KEY：commit 在 __aexit__ 之前就跑完。
    # full-suite 下 structlog 可能已被其他測試設定成 stdout renderer，
    # 因此同時檢查 caplog 與 capsys，避免 capture backend 差異造成假紅燈。
    assert commit_before_exit.await_count == 1
    captured = capsys.readouterr()
    log_text = " | ".join(
        [
            " | ".join(rec.getMessage() for rec in caplog.records),
            captured.out,
            captured.err,
        ]
    )
    assert "governance_trust_drift_auto_deprecated" in log_text, (
        f"如果 critic 主張為真（commit 在 with 外），log 不該出現。"
        f"但實際 log 出現了 → 證明 critic 主張為假。Log: {log_text!r}"
    )

    # 4. check_trust_drift 沒拋例外（_alert 內的 commit 拋錯被 try/except 吞掉，是良性的）
    assert raised is None, (
        f"check_trust_drift 不應拋例外。實際拋了：{raised}"
    )
    assert result is not None
    assert result["auto_deprecated"] == 1
