"""tests/regression/test_auto_finalize_chain_default_2529.py

회귀 테스트 — task-2529 auto finalize chain default (시스템 결함 fix).

회장 §명시 (2026-05-10):
  봇이 본질 작업과 자체 검증을 완료하면, 회장이 별도로 말하지 않아도 자동으로
  commit → push → PR → CI/Gemini → bot identity merge → smoke → reconcile까지
  진입한다. 명시적 opt-out이 없는 모든 code task는 자동 finalize chain을
  기본값으로 가진다.

회장 §명시 5 회귀 (정확히 5건):
  1. task-2524+1 사례 박제 — 자체 검증 PASS 후 PR 미생성 →
     lifecycle stuck SELF_VERIFIED_BUT_NOT_FINALIZED 자동 분류
  1-b. task-2528 사례 박제 (task-2529+1 보강, 2026-05-10) — 자체 검증 PASS 후
       commit/push/PR 미진입 → lifecycle stuck CODE_DONE_BUT_NO_COMMIT /
       SELF_VERIFIED_BUT_NOT_FINALIZED 자동 분류
  2. task md에 12단계 누락 → wrapper가 footer 자동 삽입
  3. read_only task → finalize 생략
  4. report_only task → finalize 생략
  5. code task → finalize 자동 진입 (footer 삽입 + lifecycle 정상)

본 사건 fixture (회장 §1 / §6 사례):
  - task-2524+1 (dev5 사라스와티, 2026-05-10) — 회귀/구현 PASS 후 PR 미생성
  - task-2528  (dev1 헤르메스, 2026-05-10)   — 자체 검증 PASS 후 커밋/푸시 미진입
                                                ("커밋/푸시는 별도 회장 명령 시 진행하겠습니다")

테스트 카운트 메모 (task-2529+1 정정, 2026-05-10):
  - def test_ 함수: 10개 (task-2528 fixture 추가 후)
  - parametrize 전개 후 실제 pytest collect 카운트: 15개
  - 계산: def 10 + test_regression_3(3 cases - 1) + test_regression_4(4 cases - 1) = 15
  - 봇 보고 기준 task-2529 시점 14는 def 9 + (3-1) + (4-1) = 14 — parametrize collapse가
    아니라 실제 실행 카운트로 일관됐음. 봇 보고 9건은 def test_ 함수 수, 14건은 collect 카운트.
"""
from __future__ import annotations

import sys
from pathlib import Path

import pytest

# ---------------------------------------------------------------------------
# Worktree root → sys.path (force position 0) — 패턴 task-2486/task-2523/task-2526 정합
# ---------------------------------------------------------------------------
_WORKTREE_ROOT = Path(__file__).resolve().parent.parent.parent
if str(_WORKTREE_ROOT) in sys.path:
    sys.path.remove(str(_WORKTREE_ROOT))
sys.path.insert(0, str(_WORKTREE_ROOT))

from scripts.safe_cron_dispatch import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    AUTO_FINALIZE_FOOTER_MARKER,
    AUTO_FINALIZE_OPT_OUT_TOKENS,
    DispatchStatus,
    auto_inject_finalize_footer,
    is_finalize_opt_out,
    safe_cron_dispatch,
    should_auto_inject_finalize_footer,
)
from utils.cron_targeting_audit import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    TASK_KIND_BOT,
    TASK_KIND_FOLLOWUP_RO,
    TASK_KIND_HUMAN_RESPONSE,
    TASK_KIND_INDEPENDENT,
    TASK_KIND_MERGE,
)
from utils.lifecycle_reconciliation_manager import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    LifecycleEvidence,
    StuckReason,
    detect_stuck_cases,
    determine_state,
    LifecycleState,
)


# ===========================================================================
# Fixtures
# ===========================================================================

CHAIR_CHAT = "6937032012"
DEV2_INDRA_KEY = "f3e244a7f4f0d036"


def _make_evidence(**overrides) -> LifecycleEvidence:
    """LifecycleEvidence 기본값 (FINALIZED 직전 상태) → 회귀에서 필드 override."""
    base = dict(
        task_id="task-9999",
        pr_number=None,
        pr_state=None,
        merge_commit=None,
        merged_into_main=False,
        ci_status=None,
        smoke_status=None,
        timer_status=None,
        timer_end_time=None,
        has_done=False,
        has_done_acked=False,
        has_merge_done=False,
        has_qc_result=False,
        has_followup=False,
        has_escalate_marker=False,
        escalate_marker_age_minutes=None,
        telegram_reply_truncated=False,
        bot_session_status=None,
        worktree_exists=False,
        branch_pushed_to_remote=False,
        worktree_mtime_seconds_ago=None,
        has_pushed_commits=False,
        report_artifact_present=False,
        process_alive=None,
        pr_open_age_seconds=None,
        report_artifact_age_seconds=None,
    )
    base.update(overrides)
    return LifecycleEvidence(**base)


@pytest.fixture
def audit_path(tmp_path: Path) -> Path:
    """production audit-jsonl 오염 방지 — tmp_path에 격리."""
    return tmp_path / "cron-targeting-audit.jsonl"


# ===========================================================================
# 회귀 #1 — task-2524+1 사례 박제: 자체 검증 PASS 후 PR 미생성 →
#            SELF_VERIFIED_BUT_NOT_FINALIZED 자동 분류
# ===========================================================================

def test_regression_1_task_2524_plus_1_self_verified_but_no_pr_finalize_missing():
    """본 사건 fixture: dev5 사라스와티가 회귀/구현 PASS 후 PR 미생성한 사례.

    재발 시 lifecycle_reconciliation_manager가 stuck으로 자동 분류해야 한다.
    """
    evidence = _make_evidence(
        task_id="task-2524+1",
        # 자체 검증 PASS = report artifact 작성됨 (memory/reports/task-2524+1.md)
        report_artifact_present=True,
        report_artifact_age_seconds=600.0,  # 10분 경과 (≥ 5분 임계)
        # PR 없음 — finalize chain의 commit/push/PR 단계 미진입
        pr_number=None,
        pr_state=None,
        merge_commit=None,
        # 워크트리는 존재하나 mtime은 더 오래된 상태 (10분 정도)
        worktree_exists=True,
        worktree_mtime_seconds_ago=600.0,
        has_pushed_commits=False,
        branch_pushed_to_remote=False,
        timer_status=None,
    )

    cases = detect_stuck_cases(evidence)
    reasons = {c.reason for c in cases}

    # 본 사건 핵심: SELF_VERIFIED_BUT_NOT_FINALIZED 자동 분류 필수
    assert StuckReason.SELF_VERIFIED_BUT_NOT_FINALIZED in reasons, (
        f"task-2524+1 본 사건이 stuck 분류되지 않음. cases={[c.reason.value for c in cases]}"
    )
    # 같은 신호로 CODE_DONE_BUT_NO_COMMIT도 함께 잡힐 것 — 더 구체적인 진단
    assert StuckReason.CODE_DONE_BUT_NO_COMMIT in reasons

    # determine_state도 STUCK_NEEDS_RECONCILE으로 결정해야 함
    state, _ = determine_state(evidence)
    assert state == LifecycleState.STUCK_NEEDS_RECONCILE


# ===========================================================================
# 회귀 #1-b — task-2528 사례 박제 (task-2529+1 보강, 2026-05-10):
#             자체 검증 PASS 후 commit/push/PR 미진입 →
#             SELF_VERIFIED_BUT_NOT_FINALIZED / CODE_DONE_BUT_NO_COMMIT 자동 분류
# ===========================================================================

def test_regression_1b_task_2528_dev1_hermes_self_verified_but_finalize_chain_missing():
    """task-2528 사례 박제 — 자체 검증 PASS 후 commit/push/PR 미진입.

    회장 §6 (2026-05-10) 명시 충족: task-2524 / task-2528 사례 모두 회귀 fixture에 박제.
    회장 §명시 (2026-05-10): "lifecycle stuck enum 또는 audit log만으로 task-2528
    fixture 충족을 대체하지 않는다" → 명시 def test_ 함수로 박제.

    Source:
      - dev1 헤르메스, 2026-05-10 — 자체 검증 보고서(memory/reports/task-2528.md) 작성 후
        commit/push 단계 미진입.
      - 봇 명시 발언: "커밋/푸시는 별도 회장 명령 시 진행하겠습니다"
        (auto-finalize-chain-missing-260510.jsonl).
      - 결과: AUTO_FINALIZE_CHAIN_MISSING audit log 생성, 회장 §결정으로 task-2528+1 발행.

    재발 시 lifecycle_reconciliation_manager가 자동으로 stuck 분류해야 한다.
    """
    evidence = _make_evidence(
        # 회장 § 필드 명시 (task-2524+1 fixture와 동일 수준 박제)
        task_id="task-2528",
        # 자체 검증 PASS = report artifact 작성됨 (memory/reports/task-2528.md)
        report_artifact_present=True,
        report_artifact_age_seconds=600.0,  # 10분 경과 (≥ 5분 임계)
        # commit/push 미진입 — finalize chain의 commit 단계에서 멈춤 (worktree clean,
        # last commit = 이전 task 머지)
        has_pushed_commits=False,
        branch_pushed_to_remote=False,
        # PR 없음 — gh pr list 검색 결과 0
        pr_number=None,
        pr_state=None,
        merge_commit=None,
        # 워크트리는 존재하나 mtime은 더 오래된 상태
        worktree_exists=True,
        worktree_mtime_seconds_ago=600.0,
        timer_status=None,
    )

    cases = detect_stuck_cases(evidence)
    reasons = {c.reason for c in cases}

    # 본 사건 핵심: SELF_VERIFIED_BUT_NOT_FINALIZED 또는 CODE_DONE_BUT_NO_COMMIT
    # 자동 분류 필수 (회장 §명시 4 stuck 중 매칭).
    assert (
        StuckReason.SELF_VERIFIED_BUT_NOT_FINALIZED in reasons
        or StuckReason.CODE_DONE_BUT_NO_COMMIT in reasons
    ), (
        f"task-2528 본 사건이 stuck 분류되지 않음. "
        f"cases={[c.reason.value for c in cases]}"
    )

    # 더 구체적인 진단: 두 stuck reason 모두 함께 잡혀야 함
    # (report_artifact_present=True + has_pushed_commits=False + is_stale=True
    #  → 둘 다 트리거).
    assert StuckReason.CODE_DONE_BUT_NO_COMMIT in reasons, (
        f"CODE_DONE_BUT_NO_COMMIT 미분류 — task-2528은 commit 단계 미진입 사례. "
        f"reasons={[r.value for r in reasons]}"
    )
    assert StuckReason.SELF_VERIFIED_BUT_NOT_FINALIZED in reasons, (
        f"SELF_VERIFIED_BUT_NOT_FINALIZED 미분류 — task-2528은 자체 검증 PASS 후 finalize 미완. "
        f"reasons={[r.value for r in reasons]}"
    )

    # determine_state도 STUCK_NEEDS_RECONCILE으로 결정해야 함
    state, _ = determine_state(evidence)
    assert state == LifecycleState.STUCK_NEEDS_RECONCILE


# ===========================================================================
# 회귀 #2 — task md에 12단계 누락 → wrapper가 footer 자동 삽입
# ===========================================================================

def test_regression_2_task_md_missing_12_steps_wrapper_injects_footer(audit_path: Path):
    """task md (cron prompt)에 finalize chain 명시가 빠져 있으면 wrapper가 footer를 자동 삽입.

    safe_cron_dispatch가 ALLOWED 상태로 진입하고 audit/preview에 footer가 포함된다.
    """
    bare_prompt = "[task-9001] dev2 indra: utils/feature_x.py에 새 helper 추가 + 회귀 3건"

    # 보강 (footer 미포함 검증)
    assert AUTO_FINALIZE_FOOTER_MARKER not in bare_prompt
    assert not is_finalize_opt_out(bare_prompt)

    result = safe_cron_dispatch(
        prompt=bare_prompt,
        schedule="2m",
        chat=CHAIR_CHAT,
        target_bot_key=DEV2_INDRA_KEY,
        task_kind=TASK_KIND_BOT,
        session_id=None,
        audit_path=audit_path,
    )

    # ALLOWED 흐름 — preflight 통과
    assert result.status == DispatchStatus.ALLOWED, (
        f"footer 자동 삽입은 preflight 통과해야 함. blocked_reason={result.blocked_reason}"
    )

    # command_argv 안에 prompt 인자가 들어가 있고 그 안에 footer marker가 있어야 함
    argv = list(result.command_argv)
    # cokacdir cron 인자 패턴: [..., "--cron", "<prompt>", "--at", ...]
    assert "--cron" in argv
    cron_idx = argv.index("--cron")
    injected_prompt = argv[cron_idx + 1]
    assert AUTO_FINALIZE_FOOTER_MARKER in injected_prompt, (
        "wrapper가 footer를 prompt 끝에 삽입해야 함"
    )
    # 12단계 핵심 키워드도 포함되었는지 확인 (robust해야 함)
    for keyword in ("commit", "push", "PR", "merge", "smoke", "reconcile"):
        assert keyword in injected_prompt, f"footer에 {keyword!r} 누락"


# ===========================================================================
# 회귀 #3 — read_only task는 finalize 생략
# ===========================================================================

@pytest.mark.parametrize(
    "opt_out_phrase",
    [
        "read_only: true",
        "read_only:true",  # 공백 변형
        "READ_ONLY: TRUE",  # 대소문자 변형
    ],
)
def test_regression_3_read_only_task_skips_finalize(opt_out_phrase: str, audit_path: Path):
    """read_only opt-out이 명시된 task는 자동 finalize footer가 삽입되지 않는다."""
    prompt = f"[task-9002] audit only — {opt_out_phrase}\n\nstatus scan 후 보고."

    assert is_finalize_opt_out(prompt), f"opt-out 토큰 인식 실패: {opt_out_phrase!r}"
    assert not should_auto_inject_finalize_footer(TASK_KIND_BOT, prompt)

    # 직접 함수 호출 — 멱등 + 변경 0
    out = auto_inject_finalize_footer(TASK_KIND_BOT, prompt)
    assert out == prompt, "read_only task에 footer가 삽입됨 — opt-out 위반"
    assert AUTO_FINALIZE_FOOTER_MARKER not in out

    # wrapper를 통한 end-to-end 흐름
    result = safe_cron_dispatch(
        prompt=prompt,
        schedule="2m",
        chat=CHAIR_CHAT,
        target_bot_key=DEV2_INDRA_KEY,
        task_kind=TASK_KIND_BOT,
        session_id=None,
        audit_path=audit_path,
    )
    assert result.status == DispatchStatus.ALLOWED
    cron_idx = list(result.command_argv).index("--cron")
    assert AUTO_FINALIZE_FOOTER_MARKER not in result.command_argv[cron_idx + 1]


# ===========================================================================
# 회귀 #4 — report_only / analysis_only task는 finalize 생략
# ===========================================================================

@pytest.mark.parametrize(
    "opt_out_phrase",
    [
        "report_only: true",
        "analysis_only: true",
        "finalize_policy: no_pr",
        "finalize_policy:no_pr",
    ],
)
def test_regression_4_report_only_and_analysis_only_skip_finalize(
    opt_out_phrase: str, audit_path: Path,
):
    """report_only / analysis_only / finalize_policy:no_pr 모두 finalize 생략 default."""
    prompt = f"[task-9003] retrospective task — {opt_out_phrase}\n\n회고만 작성."

    assert is_finalize_opt_out(prompt), f"opt-out 토큰 인식 실패: {opt_out_phrase!r}"
    out = auto_inject_finalize_footer(TASK_KIND_INDEPENDENT, prompt)
    assert out == prompt
    assert AUTO_FINALIZE_FOOTER_MARKER not in out

    result = safe_cron_dispatch(
        prompt=prompt,
        schedule="2m",
        chat=CHAIR_CHAT,
        target_bot_key=DEV2_INDRA_KEY,
        task_kind=TASK_KIND_INDEPENDENT,
        session_id=None,
        audit_path=audit_path,
    )
    # independent_task + no session → ALLOWED
    assert result.status == DispatchStatus.ALLOWED
    cron_idx = list(result.command_argv).index("--cron")
    assert AUTO_FINALIZE_FOOTER_MARKER not in result.command_argv[cron_idx + 1]


# ===========================================================================
# 회귀 #5 — code task는 finalize 자동 진입 + lifecycle 정상 (FINALIZED) 확인
# ===========================================================================

def test_regression_5_code_task_auto_enters_finalize_chain(audit_path: Path):
    """code task (independent / merge / bot) 모두 footer 자동 삽입 + finalize 박제 정합."""
    code_prompt = "[task-9004] dev2: utils/foo.py 변경 + 회귀 +1"

    # task_kind 3종 모두에 대해 footer 자동 삽입
    for kind in (TASK_KIND_INDEPENDENT, TASK_KIND_MERGE, TASK_KIND_BOT):
        out = auto_inject_finalize_footer(kind, code_prompt)
        assert out != code_prompt, f"{kind}에서 footer 미삽입"
        assert AUTO_FINALIZE_FOOTER_MARKER in out
        # 멱등성 — 같은 prompt를 두 번 inject해도 한 번만 추가
        twice = auto_inject_finalize_footer(kind, out)
        assert twice == out, f"{kind} footer 멱등성 위반"
        assert twice.count(AUTO_FINALIZE_FOOTER_MARKER) == 1

    # human_response / followup_readonly는 자동 footer 삽입 대상 X
    for kind in (TASK_KIND_FOLLOWUP_RO, TASK_KIND_HUMAN_RESPONSE):
        out = auto_inject_finalize_footer(kind, code_prompt)
        assert out == code_prompt, f"{kind}는 footer 자동 삽입 대상이 아님"

    # wrapper end-to-end — bot_task로 footer 자동 삽입 후 ALLOW
    result = safe_cron_dispatch(
        prompt=code_prompt,
        schedule="2m",
        chat=CHAIR_CHAT,
        target_bot_key=DEV2_INDRA_KEY,
        task_kind=TASK_KIND_BOT,
        session_id=None,
        audit_path=audit_path,
    )
    assert result.status == DispatchStatus.ALLOWED
    cron_idx = list(result.command_argv).index("--cron")
    final_prompt = result.command_argv[cron_idx + 1]
    assert AUTO_FINALIZE_FOOTER_MARKER in final_prompt

    # lifecycle 정합: FINALIZED 박제 — finalize chain 12단계 모두 완료된 evidence는
    # FINALIZED state로 분류되며 stuck 분류는 0건이어야 함.
    finalized_evidence = _make_evidence(
        task_id="task-9004",
        pr_number=99,
        pr_state="MERGED",
        merge_commit="abc1234567",
        merged_into_main=True,
        ci_status="SUCCESS",
        smoke_status="PASS",
        timer_status="completed",
        timer_end_time="2026-05-10T12:00:00Z",
        has_done=True,
        has_done_acked=True,
        has_merge_done=True,
        has_qc_result=True,
        report_artifact_present=True,
    )
    state, stuck = determine_state(finalized_evidence)
    assert state == LifecycleState.FINALIZED
    assert stuck == [], (
        "FINALIZED 상태에서 stuck 분류는 0건이어야 함 — "
        f"AUTO_FINALIZE_CHAIN_MISSING 4종이 false positive로 잡히면 안 됨. "
        f"got: {[s.reason.value for s in stuck]}"
    )


# ===========================================================================
# 보조 회귀 — opt-out 토큰 카탈로그 정합
# ===========================================================================

def test_opt_out_tokens_catalog_completeness():
    """회장 §명시 4 opt-out 토큰이 모두 카탈로그에 박제되어 있는지 정적 검증."""
    expected_logical = {"finalize_policy", "read_only", "analysis_only", "report_only"}
    found = {tok.split(":")[0].strip() for tok in AUTO_FINALIZE_OPT_OUT_TOKENS}
    missing = expected_logical - found
    assert not missing, f"opt-out 토큰 카탈로그 누락: {missing}"


def test_auto_finalize_does_not_inject_for_merged_pr_evidence():
    """이미 PR이 MERGED인 task evidence는 AUTO_FINALIZE_CHAIN_MISSING 4종 어느 것도 잡히면 안 됨."""
    merged_evidence = _make_evidence(
        task_id="task-9005",
        pr_number=100,
        pr_state="MERGED",
        merge_commit="def4567890",
        merged_into_main=True,
        ci_status="SUCCESS",
        smoke_status="PASS",
        has_done=True,
        has_done_acked=True,
        has_merge_done=True,
        timer_status="completed",
        report_artifact_present=True,
        report_artifact_age_seconds=999999.0,
        has_pushed_commits=True,
        branch_pushed_to_remote=True,
        worktree_mtime_seconds_ago=999999.0,
    )
    cases = detect_stuck_cases(merged_evidence)
    auto_finalize_reasons = {
        StuckReason.CODE_DONE_BUT_NO_COMMIT,
        StuckReason.COMMIT_DONE_BUT_NO_PR,
        StuckReason.PR_OPEN_BUT_NO_MERGE_ATTEMPT,
        StuckReason.SELF_VERIFIED_BUT_NOT_FINALIZED,
    }
    triggered = {c.reason for c in cases} & auto_finalize_reasons
    assert not triggered, (
        f"PR_MERGED 상태에서 AUTO_FINALIZE_CHAIN_MISSING이 false positive로 잡힘: {triggered}"
    )


def test_pr_open_but_no_merge_attempt_detection():
    """PR_OPEN_BUT_NO_MERGE_ATTEMPT — pr_state=OPEN + ci=SUCCESS + 5분+ 경과한 PR."""
    stuck_evidence = _make_evidence(
        task_id="task-9006",
        pr_number=200,
        pr_state="OPEN",
        merge_commit=None,
        ci_status="SUCCESS",
        pr_open_age_seconds=600.0,  # 10분 (≥ 5분 임계)
        worktree_exists=True,
        branch_pushed_to_remote=True,
        has_pushed_commits=True,
    )
    cases = detect_stuck_cases(stuck_evidence)
    reasons = {c.reason for c in cases}
    assert StuckReason.PR_OPEN_BUT_NO_MERGE_ATTEMPT in reasons, (
        f"PR_OPEN + CI SUCCESS + 10분 경과인데 stuck 분류 안 됨: {reasons}"
    )


def test_commit_done_but_no_pr_detection():
    """COMMIT_DONE_BUT_NO_PR — branch push되었지만 PR 없고 5분+ 경과."""
    stuck_evidence = _make_evidence(
        task_id="task-9007",
        pr_number=None,
        pr_state=None,
        merge_commit=None,
        worktree_exists=True,
        branch_pushed_to_remote=True,
        has_pushed_commits=True,
        worktree_mtime_seconds_ago=600.0,
    )
    cases = detect_stuck_cases(stuck_evidence)
    reasons = {c.reason for c in cases}
    assert StuckReason.COMMIT_DONE_BUT_NO_PR in reasons, (
        f"branch_push + PR missing + 10분 경과인데 stuck 분류 안 됨: {reasons}"
    )
