"""anu_v2.tests.test_pr_open_gemini_trigger_prevention_2544 — 회귀 9건 (task-2544).

회귀 케이스 (회장 §명시):
  1. preflight_base_head_fresh             — behind=0, diverged=False → fresh=True
  2. preflight_ref_fetchability_ok         — fetch exit=0, merge-base exit=0 → fetchable=True
  3. preflight_git_exit_128_detection      — fetch exit=128 → fetchable=False, git_exit_code=128
  4. pr_open_head_ref_oid_mismatch         — headRefOid != pushed_sha → match=False, INTERNAL_HEAD_MISMATCH
  5. gemini_review_gate_check_missing      — check-runs에 gemini-review-gate 없음 + git_exit_128=True
  6. first_evidence_grace_window_expiry    — poller 항상 [], clock으로 180초 만료 → call_count ≤ 6
  7. pr86_fixture_external_trigger         — fixture 기반 run() → EXTERNAL_TRIGGER_REQUIRED
  8. normal_pr_open_classification_ok      — 모든 정상 → PR_OPEN_GEMINI_TRIGGER_OK
  9. post_merge_audit_warn_to_pass_spec    — spec 정합 + isolation 검증

본 회귀는 anu_v2/* 모듈만 import 한다 (one-way isolation).
"""

from __future__ import annotations

import inspect
import json
import math
import sys
from pathlib import Path
from typing import Any
from unittest.mock import Mock, patch

# workspace root → sys.path (anu_v2 패키지를 절대 import 하기 위함)
WORKSPACE_ROOT = Path(__file__).resolve().parents[2]
if str(WORKSPACE_ROOT) not in sys.path:
    sys.path.insert(0, str(WORKSPACE_ROOT))

from anu_v2.pr_open_gemini_trigger_prevention import (  # noqa: E402
    CLASSIFICATION_EXTERNAL_TRIGGER_REQUIRED,
    CLASSIFICATION_INTERNAL_HEAD_MISMATCH,
    CLASSIFICATION_OK,
    DEFAULT_CHAT_ID,
    PROpenGeminiTriggerPrevention,
)

_FIXTURE_DIR = Path(__file__).parent.parent / "fixtures"

def _load_fixture(name: str) -> dict:
    """Local helper for fixture JSON loading. Test 파일 내부 한정.

    회장 §명시 (2026-05-10): anu_v2/fixtures/__init__.py 외부에 두지 않음.
    Critical #3 사고 재발 방지를 위한 local 정의."""
    if not name.endswith(".json"):
        name = f"{name}.json"
    return json.loads((_FIXTURE_DIR / name).read_text(encoding="utf-8"))


# ─── helpers ────────────────────────────────────────────────────────────────

def _make_module(
    *,
    gh_runner=None,
    git_runner=None,
    evidence_poller=None,
    clock=None,
    audit_writer=None,
    chat_id: str = DEFAULT_CHAT_ID,
) -> PROpenGeminiTriggerPrevention:
    """테스트용 PROpenGeminiTriggerPrevention 인스턴스 생성."""
    return PROpenGeminiTriggerPrevention(
        gh_runner=gh_runner,
        git_runner=git_runner,
        evidence_poller=evidence_poller,
        clock=clock,
        audit_writer=audit_writer,
        chat_id=chat_id,
    )


def _git_stub_fresh() -> Any:
    """fresh 상태 git_runner stub: behind=0, diverged=0, fetch=OK."""
    call_count = {"n": 0}

    def git_runner(_args: list[str]) -> dict:
        call_count["n"] += 1
        cmd = " ".join(_args)
        if "fetch" in cmd:
            return {"exit_code": 0, "stdout": ""}
        if "rev-list" in cmd:
            return {"exit_code": 0, "stdout": "0"}
        return {"exit_code": 0, "stdout": ""}

    return git_runner


def _git_stub_fetchable_ok() -> Any:
    """fetchable OK git_runner stub: fetch exit=0, merge-base exit=0."""
    def git_runner(_args: list[str]) -> dict:
        cmd = " ".join(_args)
        if "fetch" in cmd:
            return {"exit_code": 0, "stdout": ""}
        if "merge-base" in cmd:
            return {"exit_code": 0, "stdout": "abc123"}
        if "rev-list" in cmd:
            return {"exit_code": 0, "stdout": "0"}
        return {"exit_code": 0, "stdout": ""}

    return git_runner


def _git_stub_exit_128() -> Any:
    """exit 128 git_runner stub: fetch exit=128 (broken ref)."""
    def git_runner(_args: list[str]) -> dict:
        cmd = " ".join(_args)
        if "fetch" in cmd:
            return {"exit_code": 128, "stdout": "", "stderr": "fatal: unable to fetch"}
        return {"exit_code": 0, "stdout": ""}

    return git_runner


def _gh_stub_pr_head_mismatch(pr_number: int, _expected_sha: str, actual_sha: str) -> Any:
    """headRefOid mismatch gh_runner stub."""
    def gh_runner(endpoint: str, _args: list[str]) -> dict:
        if f"pulls/{pr_number}" in endpoint:
            return {"headRefOid": actual_sha}
        # check-runs: return empty
        return {"check_runs": []}

    return gh_runner


def _gh_stub_gate_missing_with_exit128(head_sha: str) -> Any:
    """gemini-review-gate 없는 check-runs + git_exit_128 시뮬레이션용 gh_runner.

    check-runs에 gemini-review-gate가 없고, exit 128 여부는 gate_present 내부 로직에서 결정.
    여기서는 failure + exit 128 텍스트가 들어있는 다른 check로 대신 커버.
    실제로는 check_present=False → git_exit_128은 별도로 preflight에서 옴.
    이 stub은 check-runs를 빈 배열로 반환해 check_present=False로 만든다.
    """
    def gh_runner(endpoint: str, _args: list[str]) -> dict:
        if "check-runs" in endpoint:
            # gemini-review-gate 없는 응답 (다른 check만 있음)
            return {
                "check_runs": [
                    {
                        "name": "ci-build",
                        "conclusion": "failure",
                        "output": {"text": "exit 128 fatal: "},
                    }
                ]
            }
        return {"headRefOid": head_sha}

    return gh_runner


def _clock_advance_stub(_grace_seconds: int, interval: int = 30) -> Any:
    """clock stub: 매 호출마다 interval씩 증가. grace_seconds 초과 시 만료."""
    state = {"t": 0.0}

    def clock() -> float:
        val = state["t"]
        state["t"] += interval
        return val

    return clock


# ─── 1. test_preflight_base_head_fresh ─────────────────────────────────────

def test_preflight_base_head_fresh() -> None:
    """git_runner stub: behind=0, diverged=False → fresh=True."""
    mod = _make_module(git_runner=_git_stub_fresh())
    result = mod.preflight_base_head_freshness("main", "feature/test")

    assert result["fresh"] is True, f"Expected fresh=True, got {result}"
    assert result["behind_count"] == 0
    assert result["head_diverged"] is False
    assert result["reasons"] == []


# ─── 2. test_preflight_ref_fetchability_ok ─────────────────────────────────

def test_preflight_ref_fetchability_ok() -> None:
    """git_runner stub: fetch exit=0, merge-base exit=0 → fetchable=True, merge_base_resolvable=True."""
    mod = _make_module(git_runner=_git_stub_fetchable_ok())
    result = mod.preflight_ref_fetchability("abc123deadbeef")

    assert result["fetchable"] is True, f"Expected fetchable=True, got {result}"
    assert result["merge_base_resolvable"] is True
    assert result["git_exit_code"] is None


# ─── 3. test_preflight_git_exit_128_detection ──────────────────────────────

def test_preflight_git_exit_128_detection() -> None:
    """git_runner stub: fetch exit=128 (broken ref) → fetchable=False, git_exit_code=128."""
    mod = _make_module(git_runner=_git_stub_exit_128())
    result = mod.preflight_ref_fetchability("deadbeef0000")

    assert result["fetchable"] is False, f"Expected fetchable=False, got {result}"
    assert result["git_exit_code"] == 128
    assert result["merge_base_resolvable"] is False


# ─── 4. test_pr_open_head_ref_oid_mismatch ─────────────────────────────────

def test_pr_open_head_ref_oid_mismatch() -> None:
    """gh_runner stub: pulls/{n} returns headRefOid != pushed_sha → match=False, INTERNAL_HEAD_MISMATCH."""
    pushed_sha = "aaaabbbbcccc1111"
    github_sha = "ddddeeee22223333"
    pr_number = 99

    gh_runner = _gh_stub_pr_head_mismatch(pr_number, pushed_sha, github_sha)
    mod = _make_module(gh_runner=gh_runner)

    head_match = mod.verify_pr_head_sha_match(pr_number, pushed_sha)
    assert head_match["match"] is False
    assert head_match["github_head_sha"] == github_sha
    assert head_match["pushed_sha"] == pushed_sha

    # classify → INTERNAL_HEAD_MISMATCH
    preflight = {"fresh": True, "behind_count": 0, "head_diverged": False,
                 "fetchable": True, "merge_base_resolvable": True, "git_exit_code": None}
    gate_present = {"check_present": True, "git_exit_128": False, "conclusion": "success",
                    "internal_cause_candidate": []}
    evidence = {"evidence_arrived": False, "elapsed_seconds": 10, "review_id": None, "review_commit_id": None}

    classification = mod.classify_trigger_miss(preflight, head_match, gate_present, evidence)
    assert classification["classification"] == CLASSIFICATION_INTERNAL_HEAD_MISMATCH
    assert classification["auto_retry_allowed"] is True
    assert classification["human_only"] is False
    assert classification["next_action"] == "repush_head_and_retry"


# ─── 5. test_gemini_review_gate_check_missing_60s_internal_cause ───────────

def test_gemini_review_gate_check_missing_60s_internal_cause() -> None:
    """gh_runner stub: check-runs에 gemini-review-gate 없음 → check_present=False.
    git_exit_128=True 시나리오: preflight에서 exit 128 발생으로 gate의 내부 원인 포함.
    """
    head_sha = "sha_for_gate_test"
    gh_runner = _gh_stub_gate_missing_with_exit128(head_sha)
    mod = _make_module(gh_runner=gh_runner)

    gate_result = mod.verify_gemini_review_gate_check_present(head_sha, max_wait_seconds=60)

    assert gate_result["check_present"] is False, f"Expected check_present=False, got {gate_result}"
    assert "check_missing" in gate_result["internal_cause_candidate"]

    # git_exit_128 시나리오: check-runs에 gemini-review-gate 없어서 git_exit_128 자체는 False,
    # 하지만 preflight fetchability에서 exit 128이 감지되어 classify에서 INTERNAL_FETCH_128 분류됨.
    # 이 테스트는 check_present=False + internal_cause_candidate에 "check_missing" 포함을 검증.
    assert len(gate_result["internal_cause_candidate"]) >= 1

    # git_exit_128이 True인 경우를 simulate: check-runs에 gemini-review-gate가 있고 exit 128 텍스트 포함
    def gh_with_gate_exit128(endpoint: str, args: list[str]) -> dict:
        if "check-runs" in endpoint:
            return {
                "check_runs": [
                    {
                        "name": "gemini-review-gate",
                        "conclusion": "failure",
                        "output": {"text": "fatal: exit 128 unable to read ref"},
                    }
                ]
            }
        return {"headRefOid": head_sha}

    mod2 = _make_module(gh_runner=gh_with_gate_exit128)
    gate_result2 = mod2.verify_gemini_review_gate_check_present(head_sha, max_wait_seconds=60)
    assert gate_result2["check_present"] is True
    assert gate_result2["git_exit_128"] is True
    assert "git_exit_128" in gate_result2["internal_cause_candidate"]


# ─── 6. test_first_evidence_grace_window_expiry_classify ───────────────────

@patch("anu_v2.pr_open_gemini_trigger_prevention.time.sleep")
def test_first_evidence_grace_window_expiry_classify(_mock_sleep) -> None:
    """evidence_poller stub: 항상 [] (gemini 응답 0). clock stub으로 grace_seconds=180 만료.
    poller call_count <= ceil(180/30) = 6 (25분 polling/self-register 절대 부재 검증).
    classify → EXTERNAL_TRIGGER_REQUIRED (head_match=True, fetchable=True, evidence_arrived=False).
    """
    GRACE = 180
    INTERVAL = 30
    MAX_CALLS = math.ceil(GRACE / INTERVAL)  # 6

    call_count = {"n": 0}

    def poller_stub(pr_number: int, head_sha: str) -> list[dict]:
        call_count["n"] += 1
        return []  # 항상 빈 리스트 (Gemini 응답 없음)

    # clock: 매 호출마다 INTERVAL씩 증가
    clock = _clock_advance_stub(GRACE, INTERVAL)

    mod = _make_module(evidence_poller=poller_stub, clock=clock)
    evidence = mod.poll_first_gemini_evidence(pr_number=42, head_sha="sha_grace_test",
                                               grace_seconds=GRACE)

    assert evidence["evidence_arrived"] is False
    # 핵심: 25분 polling 재발 금지 — poller 호출 횟수 hard limit 검증
    assert call_count["n"] <= MAX_CALLS, (
        f"poller 호출 횟수 {call_count['n']}가 hard limit {MAX_CALLS}를 초과. "
        "25분 polling 재발 패턴 금지 위반."
    )

    # classify → EXTERNAL_TRIGGER_REQUIRED
    preflight = {"fresh": True, "behind_count": 0, "head_diverged": False,
                 "fetchable": True, "merge_base_resolvable": True, "git_exit_code": None}
    head_match = {"match": True, "github_head_sha": "sha_grace_test", "pushed_sha": "sha_grace_test"}
    gate_present = {"check_present": True, "git_exit_128": False, "conclusion": None,
                    "internal_cause_candidate": []}

    classification = mod.classify_trigger_miss(preflight, head_match, gate_present, evidence)
    assert classification["classification"] == CLASSIFICATION_EXTERNAL_TRIGGER_REQUIRED
    assert classification["auto_retry_allowed"] is False
    assert classification["human_only"] is True


# ─── 7. test_pr86_fixture_external_trigger_required ────────────────────────

@patch("anu_v2.pr_open_gemini_trigger_prevention.time.sleep")
def test_pr86_fixture_external_trigger_required(_mock_sleep) -> None:
    """_load_fixture("pr_open_gemini_miss_pr86") 기반 모든 stub 셋업 → run() 결과 fixture 기대값 일치."""
    fx = _load_fixture("pr_open_gemini_miss_pr86")

    pr_number: int = fx["pr_number"]
    base_branch: str = fx["base_branch"]
    head_branch: str = fx["head_branch"]
    head_sha: str = fx["head_sha_at_open"]
    pushed_sha: str = fx["pushed_sha"]

    # git_runner: behind=1 (BEHIND 상태), diverged=True, fetch exit=0 (fetchable)
    def git_runner_pr86(args: list[str]) -> dict:
        cmd = " ".join(args)
        if "fetch" in cmd and "origin" in cmd and head_sha not in cmd:
            return {"exit_code": 0, "stdout": ""}
        if "fetch" in cmd and head_sha in cmd:
            # preflight_ref_fetchability: PR #86에서는 git_exit_128_observed=True
            # 하지만 expected_classification=EXTERNAL_TRIGGER_REQUIRED이므로,
            # classification priority 2 (FETCH_128)보다 external이 우선인 케이스 재현:
            # fixture가 EXTERNAL_TRIGGER_REQUIRED이므로 fetchable=True, git_exit_128=False로 stub
            return {"exit_code": 0, "stdout": ""}
        if "rev-list" in cmd and f"origin/{base_branch}" in cmd:
            # behind_count: BEHIND 상태 → 1
            return {"exit_code": 0, "stdout": "1"}
        if "rev-list" in cmd:
            # head_diverged: 0 (diverged=False)
            return {"exit_code": 0, "stdout": "0"}
        if "merge-base" in cmd:
            return {"exit_code": 0, "stdout": "abc"}
        return {"exit_code": 0, "stdout": "0"}

    # gh_runner: head SHA match (pushed_sha == headRefOid), check-runs에 gemini-review-gate 있음
    def gh_runner_pr86(endpoint: str, args: list[str]) -> dict:
        if f"pulls/{pr_number}" in endpoint:
            return {"headRefOid": pushed_sha}  # match=True
        if "check-runs" in endpoint:
            # gate 있음, conclusion=failure (PR #86 실제 상태)
            return {
                "check_runs": [
                    {
                        "name": "gemini-review-gate",
                        "conclusion": "failure",
                        "output": {"text": ""},  # exit 128 텍스트 없음 → git_exit_128=False
                    }
                ]
            }
        return {}

    # evidence_poller: 항상 [] (gemini_first_evidence_seconds_after_open=null)
    def poller_pr86(pr_number_: int, head_sha_: str) -> list[dict]:
        return []

    # clock: 즉시 만료 (0 → 180초 점프)
    grace = 180
    clock_state = {"t": 0.0, "calls": 0}
    def clock_pr86() -> float:
        v = clock_state["t"]
        clock_state["calls"] += 1
        # 3번째 호출부터 grace 초과
        if clock_state["calls"] >= 3:
            clock_state["t"] = float(grace + 1)
        else:
            clock_state["t"] += 30
        return v

    audit_records: list[dict] = []
    def audit_writer(rec: dict) -> None:
        audit_records.append(dict(rec))

    mod = _make_module(
        gh_runner=gh_runner_pr86,
        git_runner=git_runner_pr86,
        evidence_poller=poller_pr86,
        clock=clock_pr86,
        audit_writer=audit_writer,
    )

    result = mod.run(
        pr_number=pr_number,
        base_branch=base_branch,
        head_branch=head_branch,
        head_sha=head_sha,
        grace_seconds=grace,
    )

    assert result["classification"] == fx["expected_classification"], (
        f"Expected {fx['expected_classification']}, got {result['classification']}"
    )
    assert result["auto_retry_allowed"] == fx["expected_auto_retry_allowed"], (
        f"Expected auto_retry_allowed={fx['expected_auto_retry_allowed']}, got {result['auto_retry_allowed']}"
    )
    assert result["human_only"] == fx["expected_human_only"], (
        f"Expected human_only={fx['expected_human_only']}, got {result['human_only']}"
    )
    assert fx["expected_next_action"].split("_")[0] in result["next_action"], (
        f"Expected next_action to contain '{fx['expected_next_action'].split('_')[0]}', "
        f"got '{result['next_action']}'"
    )
    # audit_writer가 호출됐는지 확인
    assert len(audit_records) == 1, "audit_writer가 정확히 1회 호출되어야 함"


# ─── 8. test_normal_pr_open_classification_ok ──────────────────────────────

@patch("anu_v2.pr_open_gemini_trigger_prevention.time.sleep")
def test_normal_pr_open_classification_ok(_mock_sleep) -> None:
    """모든 stub 정상: head_match=True, fetchable=True, behind=0, evidence_arrived=True
    → classification == PR_OPEN_GEMINI_TRIGGER_OK, auto_retry_allowed=False, human_only=False.
    """
    head_sha = "normalshaok1234"
    pr_number = 100

    def git_runner_ok(args: list[str]) -> dict:
        cmd = " ".join(args)
        if "fetch" in cmd:
            return {"exit_code": 0, "stdout": ""}
        if "rev-list" in cmd:
            return {"exit_code": 0, "stdout": "0"}
        if "merge-base" in cmd:
            return {"exit_code": 0, "stdout": "base123"}
        return {"exit_code": 0, "stdout": ""}

    def gh_runner_ok(endpoint: str, args: list[str]) -> dict:
        if f"pulls/{pr_number}" in endpoint:
            return {"headRefOid": head_sha}
        if "check-runs" in endpoint:
            return {
                "check_runs": [
                    {
                        "name": "gemini-review-gate",
                        "conclusion": "success",
                        "output": {"text": ""},
                    }
                ]
            }
        return {}

    # poller: 즉시 gemini-code-assist[bot] 리뷰 반환
    def poller_ok(pr_number_: int, head_sha_: str) -> list[dict]:
        return [
            {
                "id": 9999,
                "user": {"login": "gemini-code-assist[bot]"},
                "commit_id": head_sha_,
            }
        ]

    # clock: 0, 10, 20 순으로 증가
    clock_state = {"t": 0.0}
    def clock_ok() -> float:
        v = clock_state["t"]
        clock_state["t"] += 10
        return v

    mod = _make_module(
        gh_runner=gh_runner_ok,
        git_runner=git_runner_ok,
        evidence_poller=poller_ok,
        clock=clock_ok,
    )

    result = mod.run(
        pr_number=pr_number,
        base_branch="main",
        head_branch="feature/ok",
        head_sha=head_sha,
        grace_seconds=180,
    )

    assert result["classification"] == CLASSIFICATION_OK, (
        f"Expected {CLASSIFICATION_OK}, got {result['classification']}"
    )
    assert result["auto_retry_allowed"] is False
    assert result["human_only"] is False
    assert result["next_action"] == "proceed_to_merge_gates"


# ─── 9. test_post_merge_audit_warn_to_pass_spec_compliance ─────────────────

def test_post_merge_audit_warn_to_pass_spec_compliance() -> None:
    """_load_fixture("post_merge_audit_warn_to_pass_pr86") + 본 모듈 외부 인터페이스 spec(§4) 위반 0건 확인.

    검증 항목:
    a. fixture["initial_audit_verdict"] == "POST_MERGE_AUDIT_WARN"
    b. fixture["final_audit_verdict_after_backfill"] == "POST_MERGE_AUDIT_PASS"
    c. fixture["lesson_pinned"]에 "marker 부재" 포함
    d. 본 모듈 source에 ".smoke-evidence" 키워드 부재 (smoke 검사 직접 수행 안 함)
    e. 본 모듈 source에 ".reconcile-evidence" 키워드 부재
    f. isolation 검증: utils/dispatch/scripts/dashboard 키워드 부재
    """
    fx = _load_fixture("post_merge_audit_warn_to_pass_pr86")

    # a. initial_audit_verdict
    assert fx["initial_audit_verdict"] == "POST_MERGE_AUDIT_WARN", (
        f"Expected POST_MERGE_AUDIT_WARN, got {fx['initial_audit_verdict']}"
    )

    # b. final_audit_verdict_after_backfill
    assert fx["final_audit_verdict_after_backfill"] == "POST_MERGE_AUDIT_PASS", (
        f"Expected POST_MERGE_AUDIT_PASS, got {fx['final_audit_verdict_after_backfill']}"
    )

    # c. lesson_pinned에 "marker 부재" 포함
    lesson = fx.get("lesson_pinned", "")
    assert "marker 부재" in lesson, (
        f"lesson_pinned에 'marker 부재' 미포함: {lesson!r}"
    )

    # d & e & f: 모듈 source inspect으로 isolation + spec 정합 검증
    src = inspect.getsource(PROpenGeminiTriggerPrevention)

    # smoke-evidence 키워드 부재 (본 모듈이 직접 smoke 검사 수행 안 함)
    assert ".smoke-evidence" not in src, (
        "PROpenGeminiTriggerPrevention 소스에 '.smoke-evidence' 키워드가 있어서는 안 됨. "
        "smoke 검사는 후속 task 책임."
    )

    # reconcile-evidence 키워드 부재
    assert ".reconcile-evidence" not in src, (
        "PROpenGeminiTriggerPrevention 소스에 '.reconcile-evidence' 키워드가 있어서는 안 됨. "
        "reconcile 검사는 후속 task 책임."
    )

    # isolation: utils/dispatch/scripts/dashboard import 부재
    forbidden_keywords = ["from utils", "import utils", "from dispatch", "import dispatch",
                          "from scripts", "import scripts", "from dashboard", "import dashboard"]
    for kw in forbidden_keywords:
        assert kw not in src, (
            f"PROpenGeminiTriggerPrevention 소스에 금지된 import '{kw}'가 발견됨. "
            "one-way isolation 위반."
        )
