"""tests/regression/test_auto_gemini_triage_2511.py — task-2511 회귀 테스트 16건.

회장 명시: PR #55/#56/#57/#61 fixture replay 포함.
모든 테스트는 100% offline — gh api / network 호출 없음.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

# workspace root → sys.path (기존 regression 테스트 패턴 준수)
WORKSPACE = Path(__file__).resolve().parent.parent.parent
if str(WORKSPACE) in sys.path:
    sys.path.remove(str(WORKSPACE))
sys.path.insert(0, str(WORKSPACE))

from utils.auto_gemini_triage import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    ThreadTriageOutcome,
    TriageVerdict,
    auto_resolve_threads,
    build_dismiss_comment,
    build_resolve_thread_args,
    build_review_gate_status,
    classify_thread,
    triage_pr,
    to_legacy_gemini_state,
)
from utils.automation_contracts import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    CriticalEscalationType,
    to_json,
)


# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------

def _make_thread(
    tid: str,
    is_outdated: bool = False,
    is_resolved: bool = False,
    body: str = "",
) -> dict:
    return {
        "id": tid,
        "isOutdated": is_outdated,
        "isResolved": is_resolved,
        "comments": [{"body": body}],
    }


# ---------------------------------------------------------------------------
# test_01_outdated_thread_auto_resolve
# ---------------------------------------------------------------------------

def test_01_outdated_thread_auto_resolve():
    thread = _make_thread("T1", is_outdated=True, body="이전 코드")
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc123",
        fix_commits=[],
        expected_files=[],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.OUTDATED
    # auto_resolve_threads 통과 후 auto_resolved=True 확인
    resolved = auto_resolve_threads([outcome], apply=False)
    assert resolved[0].auto_resolved is True


# ---------------------------------------------------------------------------
# test_02_code_already_fixed_auto_resolve
# ---------------------------------------------------------------------------

def test_02_code_already_fixed_auto_resolve():
    # commit message에 thread 본문 키워드 포함
    thread = _make_thread(
        "T2",
        body="def foo(): 함수 구현 개선 필요",
    )
    fix_commits = [
        {
            "sha": "b1eff66b1234567890abcdef",
            "message": "fix: foo 함수 구현 개선",
            "files": ["utils/auto_gemini_triage.py"],
        }
    ]
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc123",
        fix_commits=fix_commits,
        expected_files=["utils/auto_gemini_triage.py"],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.CODE_ALREADY_FIXED

    resolved = auto_resolve_threads([outcome], apply=False)
    assert resolved[0].auto_resolved is True


# ---------------------------------------------------------------------------
# test_03_false_positive_regex_dismiss
# ---------------------------------------------------------------------------

def test_03_false_positive_regex_dismiss():
    thread = _make_thread(
        "T3",
        body="grep regex가 wrapper 패턴을 false-positive로 매칭함",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc123",
        fix_commits=[],
        expected_files=[],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.FALSE_POSITIVE
    comment = build_dismiss_comment(outcome)
    assert "FALSE_POSITIVE" in comment


# ---------------------------------------------------------------------------
# test_04_style_only_dismiss
# ---------------------------------------------------------------------------

def test_04_style_only_dismiss():
    thread = _make_thread(
        "T4",
        body="nit: variable naming consistency improvement",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc123",
        fix_commits=[],
        expected_files=[],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.STYLE_ONLY

    resolved = auto_resolve_threads([outcome], apply=False)
    assert resolved[0].auto_resolved is True


# ---------------------------------------------------------------------------
# test_05_minor_fix_allowed_in_scope
# ---------------------------------------------------------------------------

def test_05_minor_fix_allowed_in_scope():
    thread = _make_thread(
        "T5",
        body="rename bool flag is_active → enabled",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc123",
        fix_commits=[],
        expected_files=["utils/auto_gemini_triage.py"],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.MINOR_FIX_ALLOWED

    resolved = auto_resolve_threads([outcome], apply=False)
    assert resolved[0].auto_resolved is True


# ---------------------------------------------------------------------------
# test_06_scope_expansion_critical
# ---------------------------------------------------------------------------

def test_06_scope_expansion_critical():
    thread = _make_thread(
        "T6",
        body="implement new module utils/new_feature.py for extended functionality",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc123",
        fix_commits=[],
        expected_files=["utils/auto_gemini_triage.py"],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.REAL_BUG_SCOPE_EXPANSION
    assert outcome.escalation_type == CriticalEscalationType.GEMINI_REAL_BUG_REQUIRES_SCOPE_EXPANSION
    assert outcome.auto_resolved is False


# ---------------------------------------------------------------------------
# test_07_forbidden_path_critical
# ---------------------------------------------------------------------------

def test_07_forbidden_path_critical():
    thread = _make_thread(
        "T7",
        body="modify dispatch.py to add hook for new functionality",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc123",
        fix_commits=[],
        expected_files=["utils/auto_gemini_triage.py"],
        forbidden_paths=["dispatch.py"],
    )
    assert outcome.verdict == TriageVerdict.REAL_BUG_SCOPE_EXPANSION
    assert outcome.escalation_type == CriticalEscalationType.FORBIDDEN_PATH_INTRUSION
    assert outcome.auto_resolved is False


# ---------------------------------------------------------------------------
# test_08_resolve_thread_payload
# ---------------------------------------------------------------------------

def test_08_resolve_thread_payload():
    args = build_resolve_thread_args("THREAD_123")
    assert isinstance(args, list)
    assert "graphql" in args
    # 어떤 element에 resolveReviewThread + THREAD_123 포함
    combined = " ".join(args)
    assert "resolveReviewThread" in combined
    assert "THREAD_123" in combined


# ---------------------------------------------------------------------------
# test_09_unresolved_count_calculation
# ---------------------------------------------------------------------------

def test_09_unresolved_count_calculation():
    # 5 OUTDATED → all auto_resolved → unresolved_count=0
    threads_5 = [
        _make_thread(f"T{i}", is_outdated=True, body="old code")
        for i in range(1, 6)
    ]
    report = triage_pr(
        pr_number=99,
        threads=threads_5,
        pr_head_sha="abc",
        fix_commits=[],
        expected_files=[],
        forbidden_paths=[],
        apply=False,
    )
    assert report.unresolved_count == 0
    assert report.auto_resolved_count == 5
    assert report.blocking_thread_count == 0

    # REAL_BUG_SCOPE_EXPANSION 1 + OUTDATED 4 → unresolved=1 (scope expansion), total unresolved=1
    threads_mix = [
        _make_thread("TX1", is_outdated=False, body="implement utils/new_module.py for feature"),
    ] + [
        _make_thread(f"TY{i}", is_outdated=True, body="old")
        for i in range(1, 5)
    ]
    report2 = triage_pr(
        pr_number=100,
        threads=threads_mix,
        pr_head_sha="abc",
        fix_commits=[],
        expected_files=["utils/auto_gemini_triage.py"],
        forbidden_paths=[],
        apply=False,
    )
    assert report2.blocking_thread_count == 1
    assert report2.unresolved_count == 1  # blocking만 unresolved


# ---------------------------------------------------------------------------
# test_10_review_gate_status_json_roundtrip
# ---------------------------------------------------------------------------

def test_10_review_gate_status_json_roundtrip():
    outcomes = [
        ThreadTriageOutcome(
            thread_id="T1",
            verdict=TriageVerdict.OUTDATED,
            auto_resolved=True,
            dismiss_comment="[AUTO-OUTDATED] ok",
            escalation_type=None,
            evidence={"outdated": True},
        )
    ]
    status = build_review_gate_status(outcomes)
    json_str = to_json(status)
    parsed = json.loads(json_str)

    assert parsed["gemini_status"] == status.gemini_status.value
    assert parsed["unresolved_threads"] == status.unresolved_threads
    assert parsed["fallback_review_used"] == status.fallback_review_used
    assert parsed["fallback_review_passed"] == status.fallback_review_passed
    assert parsed["review_gate_passed"] == status.review_gate_passed
    assert parsed["reason"] == status.reason


# ---------------------------------------------------------------------------
# test_11_pr61_replay_5_unresolved_to_clean  ★ 핵심
# ---------------------------------------------------------------------------

def test_11_pr61_replay_5_unresolved_to_clean():
    """PR #61 fixture: 5 unresolved → 5 auto-resolved → mergeStateStatus CLEAN."""
    # T1~T4: isOutdated=True
    # T5: 후속 commit b1eff66b로 반영됨
    threads = [
        _make_thread("T1", is_outdated=True, body="이전 버전 코드"),
        _make_thread("T2", is_outdated=True, body="stale comment"),
        _make_thread("T3", is_outdated=True, body="outdated review"),
        _make_thread("T4", is_outdated=True, body="old implementation"),
        _make_thread(
            "T5",
            is_outdated=False,
            body="이미 후속 commit b1eff66b로 반영됨. 코드 수정 완료.",
        ),
    ]
    fix_commits = [
        {
            "sha": "b1eff66b1234567890abcdef",
            "message": "fix: 코드 수정 완료",
            "files": ["utils/replacement_pr_runner.py"],
        }
    ]
    expected_files = [
        "utils/replacement_pr_runner.py",
        "tests/regression/test_replacement_pr_runner_2510.py",
    ]

    report = triage_pr(
        pr_number=61,
        threads=threads,
        pr_head_sha="b1eff66b1234567890abcdef",
        fix_commits=fix_commits,
        expected_files=expected_files,
        forbidden_paths=["dispatch.py"],
        apply=False,
    )

    # 5건 모두 auto_resolved=True
    assert report.auto_resolved_count == 5
    for outcome in report.threads:
        assert outcome.auto_resolved is True, f"thread {outcome.thread_id} not auto_resolved"

    # blocking=0, merge_readiness=True
    assert report.blocking_thread_count == 0
    assert report.merge_readiness is True
    assert report.unresolved_count == 0

    # legacy adapter
    legacy = to_legacy_gemini_state(report)
    assert legacy["status"] == "completed"
    assert legacy["unresolved"] == []
    assert legacy["merge_readiness"] is True


# ---------------------------------------------------------------------------
# test_12_pr56_replay_hardcoded_path_dismiss
# ---------------------------------------------------------------------------

def test_12_pr56_replay_hardcoded_path_dismiss():
    """PR #56 fixture: hardcoded path 제거 후 commit fix."""
    thread = _make_thread(
        "T56",
        body="/home/jay/projects/foo hardcoded path 제거 필요",
    )
    fix_commits = [
        {
            "sha": "deadbeef12345678",
            "message": "fix: hardcoded path 제거",
            "files": ["utils/some_module.py"],
        }
    ]
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="deadbeef",
        fix_commits=fix_commits,
        expected_files=["utils/some_module.py"],
        forbidden_paths=[],
    )
    # CODE_ALREADY_FIXED 또는 FALSE_POSITIVE
    assert outcome.verdict in (TriageVerdict.CODE_ALREADY_FIXED, TriageVerdict.FALSE_POSITIVE)
    assert outcome.auto_resolved is True


# ---------------------------------------------------------------------------
# test_13_pr57_replay_regex_grep_false_positive
# ---------------------------------------------------------------------------

def test_13_pr57_replay_regex_grep_false_positive():
    """PR #57 fixture: regex grep false-positive."""
    thread = _make_thread(
        "T57a",
        body="grep regex가 false positive를 매칭하여 오탐 발생",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc",
        fix_commits=[],
        expected_files=[],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.FALSE_POSITIVE


# ---------------------------------------------------------------------------
# test_14_pr57_replay_wrapper_helper_false_positive
# ---------------------------------------------------------------------------

def test_14_pr57_replay_wrapper_helper_false_positive():
    """PR #57 fixture: wrapper helper pattern false positive."""
    thread = _make_thread(
        "T57b",
        body="wrapper helper pattern false positive 오탐으로 보임",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc",
        fix_commits=[],
        expected_files=[],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.FALSE_POSITIVE


# ---------------------------------------------------------------------------
# test_15_pr56_replay_outdated_commit_review
# ---------------------------------------------------------------------------

def test_15_pr56_replay_outdated_commit_review():
    """PR #56 fixture: PR head 변경 후 outdated thread."""
    thread = _make_thread(
        "T56b",
        is_outdated=True,
        body="이전 commit 기준 코드 리뷰",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="newsha",
        fix_commits=[],
        expected_files=[],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.OUTDATED

    resolved = auto_resolve_threads([outcome], apply=False)
    assert resolved[0].auto_resolved is True


# ---------------------------------------------------------------------------
# test_16_pr55_replay_style_only_medium_thread
# ---------------------------------------------------------------------------

def test_16_pr55_replay_style_only_medium_thread():
    """PR #55 fixture: style-only medium thread."""
    thread = _make_thread(
        "T55",
        body="nit: prefer f-string over .format() for readability",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc",
        fix_commits=[],
        expected_files=[],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.STYLE_ONLY

    resolved = auto_resolve_threads([outcome], apply=False)
    assert resolved[0].auto_resolved is True


# ---------------------------------------------------------------------------
# Codex 권고 추가: hardcoded path direct false-positive (commit 매칭 없이)
# ---------------------------------------------------------------------------

def test_17_hardcoded_path_direct_false_positive():
    """commit 매칭 없이 HARDCODED_PATH_PATTERN만으로 FALSE_POSITIVE 직접 분류 검증.

    Codex G1 게이트 권고: PR #56 회귀가 commit 매칭으로만 통과하지 않도록
    classifier capability 자체를 검증.
    """
    thread = _make_thread(
        "T_HARD",
        body="/home/jay/projects/InsuRo/scripts/foo 경로가 절대 경로로 박혀 있음",
    )
    outcome = classify_thread(
        thread=thread,
        pr_head_sha="abc",
        fix_commits=[],   # commit 없음 — 순수 hardcoded path 패턴만 검증
        expected_files=[],
        forbidden_paths=[],
    )
    assert outcome.verdict == TriageVerdict.FALSE_POSITIVE
    assert outcome.evidence.get("matched_kind") == "hardcoded_path"


# ---------------------------------------------------------------------------
# Codex 권고 추가: review_gate_passed = blocking==0 AND unresolved==0
# ---------------------------------------------------------------------------

def test_18_review_gate_blocked_when_unresolved_remains():
    """REAL_BUG_IN_SCOPE 잔여 thread가 있으면 review_gate_passed=False.

    Codex G1 게이트 권고: blocking_thread_count == 0이어도 unresolved가 있으면
    후속 wiring(task-2514)이 머지하지 않도록 review_gate_passed를 False로 차단.
    merge_readiness는 별도 — blocking 기준만 유지.
    """
    # REAL_BUG_IN_SCOPE thread 1건 + OUTDATED 1건 합성
    thread_real = _make_thread("T_REAL", body="실제 버그: bug crash on null input")
    thread_outdated = _make_thread("T_OUT", is_outdated=True, body="이전 코드")

    report = triage_pr(
        pr_number=999,
        threads=[thread_real, thread_outdated],
        pr_head_sha="abc12345",
        fix_commits=[],
        expected_files=["utils/foo.py"],
        forbidden_paths=[],
        apply=False,
        task_id="task-test-18",
    )

    # blocking은 없음 (REAL_BUG_SCOPE_EXPANSION 아님)
    assert report.blocking_thread_count == 0
    # 그러나 REAL_BUG_IN_SCOPE는 auto_resolve 안 됨 → unresolved 잔여
    assert report.unresolved_count >= 1
    # merge_readiness는 blocking 기준 → True (blocking==0)
    assert report.merge_readiness is True
    # review_gate_passed는 unresolved==0 조건 추가 → False (잔여 있음)
    assert report.review_gate_status.review_gate_passed is False


# ---------------------------------------------------------------------------
# Codex 권고 추가: benign 코멘트 + path 언급 → false-positive/style 우선
# ---------------------------------------------------------------------------

def test_19_benign_path_mention_not_escalated_to_critical():
    """false-positive/style-only 코멘트가 path를 한번 언급해도 SCOPE_EXPANSION으로
    escalate되지 않아야 함.

    Codex G1 권고: 분류 우선순위는 false-positive/style/fixed 검사가
    expected_files scope expansion보다 먼저 실행되어야 함.
    """
    # Case A: false-positive 코멘트 with path mention
    thread_a = _make_thread(
        "T_BENIGN_FP",
        body="grep false-positive in docs/guide.md — 실제로는 정상 동작",
    )
    outcome_a = classify_thread(
        thread=thread_a,
        pr_head_sha="abc",
        fix_commits=[],
        expected_files=["utils/foo.py"],   # docs/guide.md는 expected 외
        forbidden_paths=[],
    )
    # path mention에도 불구하고 FALSE_POSITIVE로 분류되어야 함
    assert outcome_a.verdict == TriageVerdict.FALSE_POSITIVE
    assert outcome_a.auto_resolved is True

    # Case B: style-only 코멘트 with path mention
    thread_b = _make_thread(
        "T_BENIGN_STYLE",
        body="nit: rename helper variable in tests/test_x.py for readability",
    )
    outcome_b = classify_thread(
        thread=thread_b,
        pr_head_sha="abc",
        fix_commits=[],
        expected_files=["utils/foo.py"],
        forbidden_paths=[],
    )
    assert outcome_b.verdict == TriageVerdict.STYLE_ONLY
    assert outcome_b.auto_resolved is True

    # Case C: forbidden path는 여전히 우선 (보안)
    thread_c = _make_thread(
        "T_FORBIDDEN_OVERRIDE",
        body="nit: rename helper in dispatch.py",   # style hint이지만 forbidden
    )
    outcome_c = classify_thread(
        thread=thread_c,
        pr_head_sha="abc",
        fix_commits=[],
        expected_files=["utils/foo.py"],
        forbidden_paths=["dispatch.py"],
    )
    # forbidden path가 style hint보다 우선
    assert outcome_c.verdict == TriageVerdict.REAL_BUG_SCOPE_EXPANSION
    assert outcome_c.escalation_type == CriticalEscalationType.FORBIDDEN_PATH_INTRUSION
