"""anu_v2.tests.test_auto_gemini_triage_minor_in_expected_files — task-2558 회귀 9+ (회장 §명시 1:1).

회귀 케이스 (회장 §명시 task-2558 §6):
  1. medium + expected_files 내부 + 기능 영향 0 → `minor_in_expected_files`
  2. expected_files 밖 → escalation
  3. forbidden path 요구 → escalation
  4. scope expansion 요구 → escalation
  5. high real bug (severity=high) → escalation
  6. follow-up 1회 초과 (>1) → escalation
  7. cascade medium non-functional → reply+resolve 허용
  8. cascade real bug → escalation
  9. effective diff 가 expected_files 유지해야 merge 가능 (외부 경로 포함 → escalation)
 10. 17-field decision schema v1 — 필드 1:1 보장
 11. owner_trigger_decision — FIRST_MISSING / STALE / NO_ACTION + 회장 수동 fallback 금지
 12. fixture (case_001_pr_110) round-trip 호환

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

from __future__ import annotations

import json
import sys
from pathlib import Path

# 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.auto_gemini_triage import (  # noqa: E402
    ACTION_ESCALATE_SCOPE_EXPANSION,
    ACTION_REPLY_AND_RESOLVE,
    ACTION_SINGLE_FOLLOW_UP_COMMIT_ALLOWED,
    CLASSIFICATION_ESCALATION,
    CLASSIFICATION_MINOR_IN_EXPECTED_FILES,
    GEMINI_TRIAGE_DECISION_FIELDS,
    GEMINI_TRIAGE_DECISION_SCHEMA_V1,
    MAX_FOLLOW_UP_COMMITS_HARD_CAP,
    OWNER_TRIGGER_REASON_FIRST_MISSING,
    OWNER_TRIGGER_REASON_STALE_ON_HEAD,
    OWNER_TRIGGER_RESULT_EXECUTOR_SCHEDULER,
    OWNER_TRIGGER_RESULT_OK,
    CascadeFinding,
    MinorInExpectedFilesInput,
    build_gemini_triage_decision,
    classify_minor_in_expected_files,
    handle_cascade_finding,
    owner_trigger_decision,
)


# ─── helpers ────────────────────────────────────────────────────────────────
EXPECTED_DEFAULT: tuple[str, ...] = (
    "anu_v2/worktree_cleanup.py",
    "anu_v2/post_merge_smoke_runner.py",
    "anu_v2/tests/test_worktree_cleanup_2550plus1.py",
    "anu_v2/tests/test_post_merge_smoke_worktree_2550plus1.py",
)


def _baseline_input(**overrides) -> MinorInExpectedFilesInput:
    """PR #110 실전 케이스 reproduce — overrides 로 9조건 변주."""
    defaults = dict(
        task_id="task-2550+1",
        pr_number=110,
        source_thread_id="PRRT_kwDORcJVSM6BbFAM",
        severity="medium",
        path="anu_v2/worktree_cleanup.py",
        expected_files=EXPECTED_DEFAULT,
        forbidden_path_required=False,
        scope_expansion_required=False,
        functionality_impact=0,
        test_guarantee=True,
        fix_nature="clarity",
        follow_up_commits_used=1,
        effective_diff_paths=("anu_v2/worktree_cleanup.py",),
        gemini_fresh_on_new_head=True,
        ci_clean_on_new_head=True,
        merge_state_clean_on_new_head=True,
        baseline_carry_over=True,
    )
    defaults.update(overrides)
    return MinorInExpectedFilesInput(**defaults)


# ─── 회귀 1 ─ medium + expected_files 내부 + 기능 영향 0 → minor_in_expected_files ─
def test_001_medium_in_scope_no_func_impact_classifies_minor() -> None:
    ev = _baseline_input()
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_MINOR_IN_EXPECTED_FILES, report
    assert report["failed_conditions"] == []
    # 9 조건 모두 True
    assert all(report["conditions"].values())


# ─── 회귀 2 ─ expected_files 밖 → escalation ────────────────────────────────
def test_002_path_outside_expected_files_escalates() -> None:
    ev = _baseline_input(path="scripts/ci.sh")
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_ESCALATION
    assert "C2_path_in_expected_files" in report["failed_conditions"]


# ─── 회귀 3 ─ forbidden path 요구 → escalation ──────────────────────────────
def test_003_forbidden_path_required_escalates() -> None:
    ev = _baseline_input(forbidden_path_required=True)
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_ESCALATION
    assert "C3_no_forbidden_path" in report["failed_conditions"]


# ─── 회귀 4 ─ scope expansion 요구 → escalation ─────────────────────────────
def test_004_scope_expansion_required_escalates() -> None:
    ev = _baseline_input(scope_expansion_required=True)
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_ESCALATION
    assert "C4_no_scope_expansion" in report["failed_conditions"]


# ─── 회귀 5 ─ severity high → escalation (real bug) ────────────────────────
def test_005_severity_high_real_bug_escalates() -> None:
    ev = _baseline_input(severity="high")
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_ESCALATION
    assert "C1_severity_allowed" in report["failed_conditions"]


# ─── 회귀 6 ─ follow-up 1회 초과 → escalation ───────────────────────────────
def test_006_follow_up_over_cap_escalates() -> None:
    ev = _baseline_input(follow_up_commits_used=MAX_FOLLOW_UP_COMMITS_HARD_CAP + 1)
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_ESCALATION
    assert "C7_follow_up_within_cap" in report["failed_conditions"]


# ─── 회귀 7 ─ cascade medium non-functional → reply+resolve 허용 ─────────────
def test_007_cascade_non_functional_reply_and_resolve() -> None:
    cf = CascadeFinding(
        severity="medium",
        path="anu_v2/worktree_cleanup.py",
        is_real_bug=False,
        behavior_changing=False,
        forbidden_path_required=False,
        scope_expansion_required=False,
        nature="consistency",
    )
    action, detail = handle_cascade_finding(
        cf,
        follow_up_commits_used=MAX_FOLLOW_UP_COMMITS_HARD_CAP,  # cap 도달
        expected_files=EXPECTED_DEFAULT,
    )
    assert action == ACTION_REPLY_AND_RESOLVE
    assert detail["in_scope"] is True
    assert detail["reason"] in {"cap_reached_non_functional", "non_functional_in_scope"}


# ─── 회귀 8 ─ cascade real bug → escalation ────────────────────────────────
def test_008_cascade_real_bug_escalates() -> None:
    cf = CascadeFinding(
        severity="medium",
        path="anu_v2/worktree_cleanup.py",
        is_real_bug=True,
        behavior_changing=True,
        forbidden_path_required=False,
        scope_expansion_required=False,
        nature="bug",
    )
    action, detail = handle_cascade_finding(
        cf,
        follow_up_commits_used=0,
        expected_files=EXPECTED_DEFAULT,
    )
    assert action == ACTION_ESCALATE_SCOPE_EXPANSION
    assert "real_bug" in detail["factors"]


# ─── 회귀 9 ─ effective diff 가 expected_files 유지해야 merge 가능 ──────────
def test_009_effective_diff_outside_expected_escalates() -> None:
    ev = _baseline_input(
        effective_diff_paths=("anu_v2/worktree_cleanup.py", "scripts/ci.sh"),
    )
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_ESCALATION
    assert "C8_effective_diff_in_expected_files" in report["failed_conditions"]

    # 모든 effective diff 가 expected 내부이면 통과
    ev_ok = _baseline_input(
        effective_diff_paths=("anu_v2/worktree_cleanup.py",),
    )
    cls_ok, _ = classify_minor_in_expected_files(ev_ok)
    assert cls_ok == CLASSIFICATION_MINOR_IN_EXPECTED_FILES


# ─── 회귀 10 ─ 17 필드 decision schema v1 1:1 ────────────────────────────────
def test_010_decision_schema_17_fields_v1() -> None:
    ev = _baseline_input()
    decision = build_gemini_triage_decision(ev)
    assert decision["schema"] == GEMINI_TRIAGE_DECISION_SCHEMA_V1
    for field_name in GEMINI_TRIAGE_DECISION_FIELDS:
        assert field_name in decision, f"missing field: {field_name}"
    assert len(GEMINI_TRIAGE_DECISION_FIELDS) == 17
    assert decision["classification"] == CLASSIFICATION_MINOR_IN_EXPECTED_FILES
    assert decision["allowed_action"] == ACTION_SINGLE_FOLLOW_UP_COMMIT_ALLOWED
    assert decision["max_follow_up_commits"] == MAX_FOLLOW_UP_COMMITS_HARD_CAP
    assert decision["critical_escalation"] is False
    # escalation 케이스
    ev_bad = _baseline_input(severity="high")
    bad = build_gemini_triage_decision(ev_bad)
    assert bad["classification"] == CLASSIFICATION_ESCALATION
    assert bad["critical_escalation"] is True


# ─── 회귀 11 ─ owner_trigger_decision: FIRST_MISSING / STALE / NO_ACTION ────
def test_011_owner_trigger_first_missing_and_stale() -> None:
    # FIRST_GEMINI_TRIGGER_MISSING
    result, detail = owner_trigger_decision(
        follow_up_commit_pushed=False,
        prior_gemini_evidence_head="",
        current_head="324e5d03",
        first_trigger_observed=False,
    )
    assert result == OWNER_TRIGGER_RESULT_OK
    assert detail["reason"] == OWNER_TRIGGER_REASON_FIRST_MISSING
    assert detail["manual_review_fallback_forbidden"] is True

    # GEMINI_STALE_ON_HEAD (follow-up commit 후 head 변경)
    result2, detail2 = owner_trigger_decision(
        follow_up_commit_pushed=True,
        prior_gemini_evidence_head="324e5d03",
        current_head="cd594866",
        first_trigger_observed=True,
    )
    assert result2 == OWNER_TRIGGER_RESULT_OK
    assert detail2["reason"] == OWNER_TRIGGER_REASON_STALE_ON_HEAD
    assert detail2["manual_review_fallback_forbidden"] is True

    # NO_ACTION (head 동일 + first 관찰됨)
    result3, detail3 = owner_trigger_decision(
        follow_up_commit_pushed=False,
        prior_gemini_evidence_head="cd594866",
        current_head="cd594866",
        first_trigger_observed=True,
    )
    assert result3 == OWNER_TRIGGER_RESULT_EXECUTOR_SCHEDULER
    assert detail3["reason"] == "NO_ACTION_REQUIRED"
    assert detail3["manual_review_fallback_forbidden"] is True


# ─── 회귀 12 ─ fixture (case_001_pr_110) round-trip 호환 ────────────────────
def test_012_fixture_case_001_pr_110_round_trip() -> None:
    fixture_path = (
        WORKSPACE_ROOT
        / "anu_v2"
        / "fixtures"
        / "minor_in_expected_files_case_001_pr_110.json"
    )
    payload = json.loads(fixture_path.read_text())
    assert payload["schema"] == GEMINI_TRIAGE_DECISION_SCHEMA_V1
    # 17 필드 fixture 박제 검증
    for field_name in (
        "task_id",
        "pr_number",
        "source_thread_id",
        "severity",
        "path",
        "classification",
        "expected_files_internal",
        "forbidden_path_required",
        "scope_expansion_required",
        "functionality_impact",
        "baseline_carry_over",
        "allowed_action",
        "max_follow_up_commits",
        "follow_up_commits_used",
        "cascade_findings",
        "final_action",
        "critical_escalation",
    ):
        assert field_name in payload, f"fixture missing {field_name}"
    assert payload["classification"] == CLASSIFICATION_MINOR_IN_EXPECTED_FILES
    assert payload["allowed_action"] == ACTION_SINGLE_FOLLOW_UP_COMMIT_ALLOWED
    assert payload["max_follow_up_commits"] == MAX_FOLLOW_UP_COMMITS_HARD_CAP
    assert payload["follow_up_commits_used"] == 1
    assert payload["critical_escalation"] is False
    assert payload["cascade_findings"][0]["decision"] == "reply_and_resolve"
    # owner_trigger 5번째 활용 박제
    assert payload["owner_trigger_evidence"]["capability_use_count"] == 5
    assert payload["owner_trigger_evidence"]["first_use_for_classification"] == (
        "minor_in_expected_files"
    )


# ─── 회귀 13 ─ cap 미도달 + non-functional cascade → reply+resolve ──────────
def test_013_cascade_under_cap_non_functional_prefers_reply_resolve() -> None:
    cf = CascadeFinding(
        severity="medium",
        path="anu_v2/worktree_cleanup.py",
        is_real_bug=False,
        behavior_changing=False,
        forbidden_path_required=False,
        scope_expansion_required=False,
        nature="style",
    )
    action, detail = handle_cascade_finding(
        cf,
        follow_up_commits_used=0,
        expected_files=EXPECTED_DEFAULT,
    )
    assert action == ACTION_REPLY_AND_RESOLVE
    assert detail["nature"] == "style"
    assert detail["in_scope"] is True


# ─── 회귀 14 ─ cascade out-of-scope → escalate ──────────────────────────────
def test_014_cascade_out_of_scope_escalates() -> None:
    cf = CascadeFinding(
        severity="medium",
        path="scripts/ci.sh",
        is_real_bug=False,
        behavior_changing=False,
        forbidden_path_required=False,
        scope_expansion_required=False,
        nature="consistency",
    )
    action, detail = handle_cascade_finding(
        cf,
        follow_up_commits_used=0,
        expected_files=EXPECTED_DEFAULT,
    )
    assert action == ACTION_ESCALATE_SCOPE_EXPANSION
    assert detail["in_scope"] is False


# ─── 회귀 15 ─ fix_nature 외 (예: refactor) → escalation ────────────────────
def test_015_fix_nature_outside_minor_buckets_escalates() -> None:
    ev = _baseline_input(fix_nature="refactor")
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_ESCALATION
    assert "C6_fix_nature_minor" in report["failed_conditions"]


# ─── 회귀 16 ─ 새 head 재검증 미통과 → escalation ───────────────────────────
def test_016_new_head_revalidation_failure_escalates() -> None:
    ev = _baseline_input(ci_clean_on_new_head=False)
    cls, report = classify_minor_in_expected_files(ev)
    assert cls == CLASSIFICATION_ESCALATION
    assert "C9_revalidated_fresh_ci_clean" in report["failed_conditions"]
