# -*- coding: utf-8 -*-
"""tests/regression/test_merge_group_evidence_auditor_2781.py

task-2781 — merge_group evidence auditor 회귀 테스트 (12케이스 + resolver 서브케이스).

헤임달(QA) 작성. 대상 스크립트 수정 금지 — 테스트만.
"""
from __future__ import annotations

import sys
from pathlib import Path

# scripts/ 를 sys.path 에 삽입 (parents[2] = worktree 루트)
SCRIPTS = Path(__file__).resolve().parents[2] / "scripts"
sys.path.insert(0, str(SCRIPTS))

import merge_group_evidence_auditor as auditor
import merge_group_pr_resolver as resolver

import pytest


# ── 헬퍼 ─────────────────────────────────────────────────────────────────────

def make_gate(state="pass", reason="ok", primary=None, high=None, elapsed=0):
    """evaluate_gate 가짜 반환 헬퍼."""
    return {
        "state": state,
        "reason": reason,
        "evidence": {
            "primary": primary if primary is not None else [],
            "secondary": [],
            "high_severity_hits": high if high is not None else [],
            "supplementary_signals": [],
        },
        "head_pushed_at": "",
        "elapsed_seconds": elapsed,
        "timeout_seconds": 300,
    }


def fresh_entry(commit_sha="abc123"):
    """stale=False 인 primary evidence entry."""
    return {
        "type": "review",
        "id": 1,
        "body_snippet": "",
        "commit_sha": commit_sha,
        "stale": False,
        "severity_matches": [],
    }


def stale_entry(commit_sha="oldsha"):
    """stale=True 인 primary evidence entry."""
    return {
        "type": "review",
        "id": 2,
        "body_snippet": "",
        "commit_sha": commit_sha,
        "stale": True,
        "severity_matches": [],
    }


# ── 케이스 01 ─────────────────────────────────────────────────────────────────

def test_case01_single_pr_fresh_evidence_pass(monkeypatch):
    """케이스 01: 단일 PR, evidence fresh, HIGH0 → PASS."""
    PR_NUMBER = 101
    PR_HEAD_SHA = "sha_pr101"
    MG_SHA = "sha_mg01"
    REPO = "owner/repo"

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: make_gate(
        "pass", reason="valid evidence", primary=[fresh_entry(PR_HEAD_SHA)]
    ))
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: PR_HEAD_SHA)
    monkeypatch.setattr(auditor, "fetch_owner_resolve_pending", lambda repo, pr_number: False)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.PASS, f"Expected PASS, got {result['status']}: {result['reason']}"


# ── 케이스 02 ─────────────────────────────────────────────────────────────────

def test_case02_single_pr_no_evidence_missing(monkeypatch):
    """케이스 02: 단일 PR, evidence 없음 → GEMINI_EVIDENCE_SKIPPED_OR_MISSING."""
    PR_NUMBER = 102
    PR_HEAD_SHA = "sha_pr102"
    MG_SHA = "sha_mg02"
    REPO = "owner/repo"

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: make_gate(
        "block", reason="evidence timeout (5min exceeded), no valid evidence", primary=[]
    ))
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: PR_HEAD_SHA)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.GEMINI_EVIDENCE_SKIPPED_OR_MISSING, \
        f"Expected GEMINI_EVIDENCE_SKIPPED_OR_MISSING, got {result['status']}"


# ── 케이스 03 ─────────────────────────────────────────────────────────────────

def test_case03_check_skipped_success_but_no_primary_evidence(monkeypatch):
    """케이스 03: gemini-review-gate check가 skipped=success인데 evidence 없음
    → check 무시하고 fail (GEMINI_EVIDENCE_SKIPPED_OR_MISSING).
    check_run conclusion="success" 가 있어도 primary=[] 이면 auditor는 무조건 fail."""
    PR_NUMBER = 103
    PR_HEAD_SHA = "sha_pr103"
    MG_SHA = "sha_mg03"
    REPO = "owner/repo"

    # secondary에 check_run conclusion=success 넣되 primary는 비어 있음
    gate_with_check = {
        "state": "block",
        "reason": "evidence timeout (5min exceeded), no valid evidence",
        "evidence": {
            "primary": [],
            "secondary": [
                {
                    "type": "check_run",
                    "id": 999,
                    "name": "gemini-review-gate",
                    "status": "completed",
                    "conclusion": "success",
                    "app_slug": "gemini-code-assist",
                }
            ],
            "high_severity_hits": [],
            "supplementary_signals": [],
        },
        "head_pushed_at": "",
        "elapsed_seconds": 0,
        "timeout_seconds": 300,
    }

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: gate_with_check)
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: PR_HEAD_SHA)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    # check_run success 여도 primary evidence 없으면 fail
    assert result["status"] == auditor.GEMINI_EVIDENCE_SKIPPED_OR_MISSING, \
        f"Expected GEMINI_EVIDENCE_SKIPPED_OR_MISSING (check success ignored), got {result['status']}"


# ── 케이스 04 ─────────────────────────────────────────────────────────────────

def test_case04_evidence_stale_sha_mismatch(monkeypatch):
    """케이스 04: evidence commit_sha != pr_head_sha (all stale) → GEMINI_EVIDENCE_STALE."""
    PR_NUMBER = 104
    PR_HEAD_SHA = "sha_pr104"
    MG_SHA = "sha_mg04"
    REPO = "owner/repo"

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: make_gate(
        "block", reason="all evidence stale (SHA mismatch)", primary=[stale_entry("old_sha")]
    ))
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: PR_HEAD_SHA)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.GEMINI_EVIDENCE_STALE, \
        f"Expected GEMINI_EVIDENCE_STALE, got {result['status']}"


# ── 케이스 05 ─────────────────────────────────────────────────────────────────

def test_case05_trap1_auditor_uses_pr_head_sha_not_merge_group_sha(monkeypatch):
    """케이스 05: 함정1 회귀방지 — auditor가 evaluate_gate에 pr_head_sha를 전달하는지 검증.
    fake evaluate_gate는 head_sha==pr_head_sha일 때만 pass 반환,
    merge_group_sha 등 다른 값이면 block 반환.
    audit_merge_group PASS여야 하고 기록된 호출 sha가 pr_head_sha임을 assert."""
    PR_NUMBER = 105
    PR_HEAD_SHA = "correct_pr_head_sha_105"
    MG_SHA = "merge_group_sha_105"  # 실수로 넘기면 fail
    REPO = "owner/repo"

    called_with_shas = []

    def fake_evaluate_gate(pr_number, head_sha, repo):
        called_with_shas.append(head_sha)
        if head_sha == PR_HEAD_SHA:
            return make_gate("pass", primary=[fresh_entry(PR_HEAD_SHA)])
        else:
            # merge_group_sha 나 다른 sha가 왔으면 block (evidence 없음)
            return make_gate("block", primary=[])

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", fake_evaluate_gate)
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: PR_HEAD_SHA)
    monkeypatch.setattr(auditor, "fetch_owner_resolve_pending", lambda repo, pr_number: False)

    result = auditor.audit_merge_group(REPO, MG_SHA)

    # PASS 여야 함 (auditor가 pr_head_sha를 올바르게 넘긴 경우에만)
    assert result["status"] == auditor.PASS, \
        f"함정1: auditor가 merge_group_sha를 넘기면 block 반환 — got {result['status']}"

    # 호출된 sha가 pr_head_sha여야 함 (merge_group_sha가 아님)
    assert len(called_with_shas) >= 1
    assert called_with_shas[0] == PR_HEAD_SHA, \
        f"함정1: evaluate_gate에 전달된 sha={called_with_shas[0]!r}, 기대={PR_HEAD_SHA!r}"
    assert MG_SHA not in called_with_shas, \
        f"함정1: merge_group_sha가 evaluate_gate에 전달됨: {called_with_shas}"


# ── 케이스 06 ─────────────────────────────────────────────────────────────────

def test_case06_head_advanced_after_admission(monkeypatch):
    """케이스 06: admission 이후 PR head 전진 → HEAD_ADVANCED_AFTER_ADMISSION."""
    PR_NUMBER = 106
    PR_HEAD_SHA = "admitted_sha_106"
    CURRENT_SHA = "newer_sha_106"  # 전진됨
    MG_SHA = "sha_mg06"
    REPO = "owner/repo"

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: make_gate(
        "pass", primary=[fresh_entry(PR_HEAD_SHA)]
    ))
    # fetch_current_pr_head 가 다른 sha 반환 → HEAD_ADVANCED
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: CURRENT_SHA)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.HEAD_ADVANCED_AFTER_ADMISSION, \
        f"Expected HEAD_ADVANCED_AFTER_ADMISSION, got {result['status']}"


# ── 케이스 07 ─────────────────────────────────────────────────────────────────

def test_case07_unresolved_high_severity(monkeypatch):
    """케이스 07: unresolved HIGH 1건 → UNRESOLVED_HIGH_OR_CRITICAL."""
    PR_NUMBER = 107
    PR_HEAD_SHA = "sha_pr107"
    MG_SHA = "sha_mg07"
    REPO = "owner/repo"

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: make_gate(
        "block",
        reason="high severity matched: ['severity:high/critical']",
        primary=[fresh_entry(PR_HEAD_SHA)],
        high=["severity:high/critical"],
    ))
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: PR_HEAD_SHA)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.UNRESOLVED_HIGH_OR_CRITICAL, \
        f"Expected UNRESOLVED_HIGH_OR_CRITICAL, got {result['status']}"


# ── 케이스 08 ─────────────────────────────────────────────────────────────────

def test_case08_batch_3prs_one_stale_all_fail(monkeypatch):
    """케이스 08: batch 3 PR 중 1개 stale → 전체 fail (GEMINI_EVIDENCE_STALE)."""
    PR1 = {"pr_number": 181, "pr_head_sha": "sha181"}
    PR2 = {"pr_number": 182, "pr_head_sha": "sha182"}
    PR3 = {"pr_number": 183, "pr_head_sha": "sha183"}
    MG_SHA = "sha_mg08"
    REPO = "owner/repo"

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [PR1, PR2, PR3])

    def fake_gate(pr_number, head_sha, repo):
        if pr_number == 183:
            # PR3: all stale
            return make_gate("block", reason="all evidence stale", primary=[stale_entry("old183")])
        else:
            # PR1, PR2: pass
            sha = PR1["pr_head_sha"] if pr_number == 181 else PR2["pr_head_sha"]
            return make_gate("pass", primary=[fresh_entry(sha)])

    monkeypatch.setattr(auditor, "evaluate_gate", fake_gate)

    def fake_current_head(repo, pr_number):
        mapping = {181: "sha181", 182: "sha182", 183: "sha183"}
        return mapping[pr_number]

    monkeypatch.setattr(auditor, "fetch_current_pr_head", fake_current_head)
    monkeypatch.setattr(auditor, "fetch_owner_resolve_pending", lambda repo, pr_number: False)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.GEMINI_EVIDENCE_STALE, \
        f"Expected GEMINI_EVIDENCE_STALE (1 stale fails all), got {result['status']}"
    assert result["status"] != auditor.PASS


# ── 케이스 09 ─────────────────────────────────────────────────────────────────

def test_case09a_resolve_fails_merge_group_prs_unresolved(monkeypatch):
    """케이스 09a: 역추적 API 실패 + fallback 실패 → MERGE_GROUP_PRS_UNRESOLVED
    (auditor.resolve_merge_group_prs 가 MergeGroupPRsUnresolved raise)."""
    MG_SHA = "sha_mg09"
    REPO = "owner/repo"

    def fake_resolve(**kw):
        raise resolver.MergeGroupPRsUnresolved("All 3 tiers failed")

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", fake_resolve)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.MERGE_GROUP_PRS_UNRESOLVED, \
        f"Expected MERGE_GROUP_PRS_UNRESOLVED, got {result['status']}"


def test_case09b_resolver_unit_all_tiers_fail_raises(monkeypatch):
    """케이스 09b: resolver 단위 테스트 — resolve_via_api/event_context/ref_and_trailers 모두
    [] 반환 → resolve_merge_group_prs 가 MergeGroupPRsUnresolved raise."""
    monkeypatch.setattr(resolver, "resolve_via_api", lambda *a, **kw: [])
    monkeypatch.setattr(resolver, "resolve_via_event_context", lambda *a, **kw: [])
    monkeypatch.setattr(resolver, "resolve_via_ref_and_trailers", lambda *a, **kw: [])

    with pytest.raises(resolver.MergeGroupPRsUnresolved):
        resolver.resolve_merge_group_prs(
            repo="owner/repo",
            merge_group_sha="sha_mg09b",
            base_sha=None,
            head_ref="gh-readonly-queue/main/pr-9-base",
            event={"merge_group": {"head_ref": "gh-readonly-queue/main/pr-9-base"}},
        )


# ── 케이스 10 ─────────────────────────────────────────────────────────────────

def test_case10_resolver_api_wins_over_ref(monkeypatch):
    """케이스 10: 역추적 API 성공, ref 파싱 불일치 → API 우선.
    resolve_via_api=[{pr_number:1, pr_head_sha:'aaa'}], resolve_via_ref_and_trailers=[{pr_number:2,'bbb'}]
    결과는 API의 것이고 ref 함수가 호출되지 않았는지 검증."""
    api_result = [{"pr_number": 1, "pr_head_sha": "aaa"}]
    ref_call_count = []

    def fake_api(repo, merge_group_sha, base_sha=None):
        return api_result

    def fake_ref(repo, head_ref, base_sha, merge_group_sha):
        ref_call_count.append(1)
        return [{"pr_number": 2, "pr_head_sha": "bbb"}]

    monkeypatch.setattr(resolver, "resolve_via_api", fake_api)
    monkeypatch.setattr(resolver, "resolve_via_event_context", lambda *a, **kw: [])
    monkeypatch.setattr(resolver, "resolve_via_ref_and_trailers", fake_ref)

    result = resolver.resolve_merge_group_prs(
        repo="owner/repo",
        merge_group_sha="sha_mg10",
        base_sha="base10",
        head_ref="gh-readonly-queue/main/pr-1-base10",
        event=None,
    )

    # API 결과를 사용해야 함
    assert len(result) == 1
    assert result[0]["pr_number"] == 1
    assert result[0]["pr_head_sha"] == "aaa"

    # ref 함수는 호출되지 않아야 함 (API가 1차 신뢰원 → fall-through 없음)
    assert len(ref_call_count) == 0, \
        f"resolve_via_ref_and_trailers 가 호출됨 (API 우선 위반): 호출 횟수={len(ref_call_count)}"


# ── 케이스 11 ─────────────────────────────────────────────────────────────────

def test_case11_trap2_long_elapsed_fresh_evidence_still_passes(monkeypatch):
    """케이스 11: 함정2 회귀방지 — queue 대기 500초 경과, evidence fresh → PASS.
    auditor는 elapsed_seconds를 무시하고 gate state=pass면 PASS.
    wall-clock 미적용 회귀 방지."""
    PR_NUMBER = 111
    PR_HEAD_SHA = "sha_pr111"
    MG_SHA = "sha_mg11"
    REPO = "owner/repo"

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    # elapsed_seconds=500 이어도 state=pass면 통과해야 함
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: make_gate(
        "pass",
        reason="valid evidence",
        primary=[fresh_entry(PR_HEAD_SHA)],
        elapsed=500,  # 500초 경과 — auditor가 이를 차단하면 안 됨
    ))
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: PR_HEAD_SHA)
    monkeypatch.setattr(auditor, "fetch_owner_resolve_pending", lambda repo, pr_number: False)

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.PASS, \
        f"함정2: elapsed=500s일 때 PASS가 아님. got {result['status']}: {result['reason']}"


# ── 케이스 12 ─────────────────────────────────────────────────────────────────

def test_case12_internal_exception_fail_closed(monkeypatch):
    """케이스 12: 내부 예외 → AUDITOR_INTERNAL_ERROR fail-closed.
    evaluate_gate가 Exception raise → audit_pr 예외를 잡아 AUDITOR_INTERNAL_ERROR."""
    PR_NUMBER = 112
    PR_HEAD_SHA = "sha_pr112"
    MG_SHA = "sha_mg12"
    REPO = "owner/repo"

    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: (_ for _ in ()).throw(
        Exception("simulated internal failure")
    ))

    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.AUDITOR_INTERNAL_ERROR, \
        f"Expected AUDITOR_INTERNAL_ERROR (fail-closed), got {result['status']}"


# ── 케이스 13 ─────────────────────────────────────────────────────────────────

def test_case13_unresolved_verdict_has_diagnostics(monkeypatch):
    """케이스 13: 모든 tier 실패 시 verdict JSON에 tier별 시도/거부 사유가 담긴 diagnostics 존재."""
    monkeypatch.setattr(resolver, "resolve_via_api", lambda *a, **k: [])
    monkeypatch.setattr(resolver, "resolve_via_event_context", lambda *a, **k: [])
    monkeypatch.setattr(resolver, "resolve_via_ref_and_trailers", lambda *a, **k: [])
    result = auditor.audit_merge_group(
        "owner/repo", "mg13",
        head_ref="gh-readonly-queue/main/pr-13-base",
        event={"merge_group": {"head_ref": "gh-readonly-queue/main/pr-13-base"}},
    )
    assert result["status"] == auditor.MERGE_GROUP_PRS_UNRESOLVED
    assert "diagnostics" in result, "UNRESOLVED verdict에 diagnostics 필드 없음"
    diag = result["diagnostics"]
    tiers = diag["tiers"]
    tier_names = {t["tier"] for t in tiers}
    assert {"api", "event", "ref"} <= tier_names, f"tier 누락: {tier_names}"
    for t in tiers:
        if t["invoked"]:
            assert "reject_reason" in t and t["reject_reason"] is not None
            assert "entry_count" in t and "accepted" in t and "gh" in t


# ── 케이스 14 ─────────────────────────────────────────────────────────────────

def test_case14_gh_api_failure_recorded_in_diagnostics(monkeypatch):
    """케이스 14: gh api 실패(rc!=0 + stderr) 시 diagnostics의 gh 항목에 rc·stderr_snip·api_path·tier 존재."""
    class FakeProc:
        def __init__(self, rc, out, err):
            self.returncode = rc; self.stdout = out; self.stderr = err
    def fake_run(cmd, **kw):
        return FakeProc(1, "", "gh: Not Found (HTTP 404) error body")
    monkeypatch.setattr(resolver.subprocess, "run", fake_run)
    with pytest.raises(resolver.MergeGroupPRsUnresolved) as ei:
        resolver.resolve_merge_group_prs(
            repo="owner/repo", merge_group_sha="mg14", base_sha="base14",
            head_ref="gh-readonly-queue/main/pr-42-base14",
            event={"merge_group": {"head_ref": "gh-readonly-queue/main/pr-42-base14"}},
        )
    diags = ei.value.diagnostics
    gh_entries = [g for t in diags for g in t.get("gh", [])]
    assert gh_entries, "gh 실패 기록이 비어있음"
    assert any(
        g.get("rc") == 1 and "404" in g.get("stderr_snip", "")
        and g.get("api_path") and g.get("tier")
        for g in gh_entries
    ), f"rc/stderr_snip/api_path/tier 완비 항목 없음: {gh_entries}"
    # api_path 다양성: graphql(api tier) + repos/.../pulls/42(ref tier) 최소 1개씩 기대
    api_paths = {g["api_path"] for g in gh_entries}
    assert "graphql" in api_paths


# ── 케이스 15 ─────────────────────────────────────────────────────────────────

def test_case15_pass_case_unchanged_no_diagnostics(monkeypatch):
    """케이스 15: PASS 케이스의 status/기존 필드 불변 + diagnostics 미추가(순수 additive 증명)."""
    PR_NUMBER = 115; PR_HEAD_SHA = "sha_pr115"; MG_SHA = "sha_mg15"; REPO = "owner/repo"
    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": PR_NUMBER, "pr_head_sha": PR_HEAD_SHA}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: make_gate(
        "pass", reason="valid evidence", primary=[fresh_entry(PR_HEAD_SHA)]
    ))
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: PR_HEAD_SHA)
    monkeypatch.setattr(auditor, "fetch_owner_resolve_pending", lambda repo, pr_number: False)
    result = auditor.audit_merge_group(REPO, MG_SHA)
    assert result["status"] == auditor.PASS
    for k in ("status", "reason", "pr_results", "merge_group_sha", "resolved_prs"):
        assert k in result, f"기존 필드 누락: {k}"
    assert "diagnostics" not in result, "PASS 경로에 diagnostics가 추가됨(additive 위반)"


# ── 케이스 16 ─────────────────────────────────────────────────────────────────

def test_case16_diagnostics_contains_repo_slug(monkeypatch):
    """케이스 16: diagnostics에 실제 조회한 repo slug 포함(설계 §5)."""
    monkeypatch.setattr(resolver, "resolve_via_api", lambda *a, **k: [])
    monkeypatch.setattr(resolver, "resolve_via_event_context", lambda *a, **k: [])
    monkeypatch.setattr(resolver, "resolve_via_ref_and_trailers", lambda *a, **k: [])
    REPO = "myowner/myrepo"
    result = auditor.audit_merge_group(
        REPO, "mg16",
        head_ref="gh-readonly-queue/main/pr-16-base",
        event={"merge_group": {"head_ref": "gh-readonly-queue/main/pr-16-base"}},
    )
    assert result["status"] == auditor.MERGE_GROUP_PRS_UNRESOLVED
    assert result["diagnostics"]["repo"] == REPO, \
        f"diagnostics에 조회 repo slug 불일치: {result['diagnostics']['repo']!r}"


# ── 케이스 17 ─────────────────────────────────────────────────────────────────

def test_case17_no_publish_check_no_check_run(monkeypatch):
    """케이스 17: --publish-check 없는 실행에서 check-run 생성 0 (설계 테스트요구 4)."""
    calls = []
    monkeypatch.setattr(auditor, "publish_check_run", lambda *a, **k: calls.append(1) or {})
    monkeypatch.setattr(auditor, "resolve_merge_group_prs", lambda **kw: [
        {"pr_number": 170, "pr_head_sha": "sha170"}
    ])
    monkeypatch.setattr(auditor, "evaluate_gate", lambda pr_number, head_sha, repo: make_gate(
        "pass", primary=[fresh_entry("sha170")]
    ))
    monkeypatch.setattr(auditor, "fetch_current_pr_head", lambda repo, pr_number: "sha170")
    monkeypatch.setattr(auditor, "fetch_owner_resolve_pending", lambda repo, pr_number: False)
    monkeypatch.setattr(sys, "argv", ["auditor", "--merge-group-sha", "mg17", "--repo", "owner/repo"])
    rc = auditor.main()
    assert rc == 0, f"PASS 시 exit 0 기대, got {rc}"
    assert len(calls) == 0, f"--publish-check 없는데 check-run 발행됨: {len(calls)}회"


# ── 케이스 18 (PR266 contextvars microfix): resolver context 밖 gh 실패 → sink 누수 0 ──

def test_case18_gh_wrapper_outside_resolver_context_no_leak(monkeypatch):
    """resolve_merge_group_prs 밖에서 _gh_api/_gh_graphql 실패가 나도 diagnostics sink에
    누적되지 않는다(contextvars default=None → no-op, 누수 없음)."""
    class FakeProc:
        def __init__(self, rc, out, err):
            self.returncode = rc; self.stdout = out; self.stderr = err
    monkeypatch.setattr(resolver.subprocess, "run",
                        lambda cmd, **kw: FakeProc(1, "", "boom outside context"))
    # resolver context 밖: sink는 default None
    assert resolver._GH_DIAG_SINK.get() is None
    rc1, _ = resolver._gh_api("repos/x/y/pulls/1")   # 실패(rc=1)
    rc2, _ = resolver._gh_graphql("query { x }")      # 실패(rc=1)
    assert rc1 == 1 and rc2 == 1
    # ★ 누수 없음: context 밖 실패는 sink에 누적 0
    assert resolver._GH_DIAG_SINK.get() is None, "context 밖 gh 실패가 sink에 누적됨(누수)"


# ── 케이스 19 (PR266 contextvars microfix): 병렬(스레드) resolve → diagnostics 격리 ──

def test_case19_concurrent_resolve_diagnostics_isolated(monkeypatch):
    """병렬 스레드에서 resolve_merge_group_prs를 동시에 호출해도 각 호출의 diagnostics가
    서로 섞이지 않는다(contextvars per-context 격리). 호출 종료 후 context 복원(None)."""
    import threading
    class FakeProc:
        def __init__(self, rc, out, err):
            self.returncode = rc; self.stdout = out; self.stderr = err
    monkeypatch.setattr(resolver.subprocess, "run",
                        lambda cmd, **kw: FakeProc(1, "", "err"))
    monkeypatch.setattr(resolver, "resolve_via_event_context", lambda *a, **k: [])
    results = {}
    barrier = threading.Barrier(3)

    def worker(tag):
        barrier.wait()  # 동시 진입 강제
        try:
            resolver.resolve_merge_group_prs(repo=f"owner/{tag}", merge_group_sha=f"mg-{tag}")
        except resolver.MergeGroupPRsUnresolved as e:
            results[tag] = (e.repo, e.merge_group_sha, len(e.diagnostics))

    threads = [threading.Thread(target=worker, args=(t,)) for t in ("A", "B", "C")]
    for t in threads: t.start()
    for t in threads: t.join()

    # 각 스레드는 자기 repo/sha/diagnostics만 가짐(교차오염 0)
    for tag in ("A", "B", "C"):
        assert tag in results, f"{tag} 스레드 결과 없음"
        repo, mg, ndiag = results[tag]
        assert repo == f"owner/{tag}" and mg == f"mg-{tag}", f"{tag} 교차오염: {results[tag]}"
        assert ndiag >= 1, f"{tag} diagnostics 비어있음"
    # 메인 스레드 context는 건드려지지 않음(누수 없음)
    assert resolver._GH_DIAG_SINK.get() is None


# ── 케이스 20 (PR266 microfix): bytes stderr → diagnostics JSON 직렬화 안전 ──

def test_case20_bytes_stderr_diagnostics_json_serializable(monkeypatch):
    """subprocess가 text=False 등으로 bytes stderr를 반환해도 diagnostics의 stderr_snip이
    str로 안전 변환되어 verdict JSON(json.dumps) 직렬화가 깨지지 않는다(신규 HIGH 방어)."""
    import json as _json
    class FakeProc:
        def __init__(self, rc, out, err):
            self.returncode = rc; self.stdout = out; self.stderr = err
    # ★ bytes stderr 주입 (rc!=0 실패 경로)
    monkeypatch.setattr(resolver.subprocess, "run",
                        lambda cmd, **kw: FakeProc(1, "", b"gh: Not Found (HTTP 404) \xff\xfe bytes"))
    monkeypatch.setattr(resolver, "resolve_via_event_context", lambda *a, **k: [])
    with pytest.raises(resolver.MergeGroupPRsUnresolved) as ei:
        resolver.resolve_merge_group_prs(repo="owner/repo", merge_group_sha="mg20")
    diags = ei.value.diagnostics
    gh_entries = [g for t in diags for g in t.get("gh", [])]
    assert gh_entries, "gh 실패 기록이 비어있음"
    # 모든 stderr_snip이 str
    for g in gh_entries:
        assert isinstance(g.get("stderr_snip"), str), f"stderr_snip이 str 아님: {type(g.get('stderr_snip'))}"
    # ★ verdict JSON 직렬화가 TypeError 없이 성공
    payload = {"status": resolver.__name__, "diagnostics": {"tiers": diags}}
    s = _json.dumps(payload, ensure_ascii=False)
    assert "404" in s
