"""통합 회귀 테스트 — PostMergeSmokeRunner + WorktreeCleanup 통합 (task-2550+1).

PR #100 medium unresolved fix 회귀:
  - medium #3: `r.all_safe` 가 dry-run 에서 항상 False → cleanup_candidates 0 (가시성 상실).
                fix 후: `is_safe_ignoring_apply` helper 사용 → 1~5 PASS 만 검사하여 후보 산정.

pytest 사용. 외부 부수효과 (subprocess / file write / clock) 는 모두 fake callable 로 주입.

⚠️ 테스트 코드 내 raw token placeholder 는 실제 토큰 아님.
"""

from __future__ import annotations

import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable

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.post_merge_smoke_runner import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    PostMergeSmokeRunner,
)

# ─── 공용 ────────────────────────────────────────────────────────────────────
FAKE_SHA40 = "a" * 40
FAKE_MAIN_SHA = "b" * 40
FAKE_TASK_ID = "task-2550+1"
FAKE_BRANCH = "task/task-2550+1-dev5"


def _fake_clock() -> datetime:
    return datetime(2026, 5, 12, 12, 0, 0, tzinfo=timezone.utc)


def _proc(returncode: int = 0, stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess:
    return subprocess.CompletedProcess(args=[], returncode=returncode, stdout=stdout, stderr=stderr)


def _make_smoke_pass_runner(
    *,
    worktree_porcelain: str = "",
    pr_state: str = "MERGED",
    pr_head_ref: str = FAKE_BRANCH,
) -> Callable[..., subprocess.CompletedProcess]:
    """smoke PASS + worktree cleanup 지원 fake runner."""
    pr_response = json.dumps([
        {"number": 999, "state": pr_state, "headRefName": pr_head_ref}
    ])

    def fake_runner(args, **_):
        if "rev-parse" in args:
            return _proc(0, FAKE_MAIN_SHA + "\n", "")
        # smoke command 매칭 — args[0] 가 python3 / pytest 인 경우만 (path 우연 매칭 회피)
        if args and (str(args[0]).endswith("python3") or str(args[0]).endswith("pytest")):
            return _proc(0, "smoke ok", "")
        if "worktree" in args and "list" in args and "--porcelain" in args:
            return _proc(0, worktree_porcelain, "")
        if "status" in args and "--porcelain" in args:
            return _proc(0, "", "")
        if "pr" in args and "list" in args:
            return _proc(0, pr_response, "")
        if "merge-base" in args:
            return _proc(0, "", "")
        if "pgrep" in args:
            return _proc(1, "", "")
        if args and args[0] == "lsof":
            return _proc(1, "", "")
        return _proc(0, "", "")

    return fake_runner


def _make_smoke_fail_runner() -> Callable[..., subprocess.CompletedProcess]:
    def fake_runner(args, **_):
        if "rev-parse" in args:
            return _proc(0, FAKE_MAIN_SHA + "\n", "")
        return _proc(1, "", "smoke failed")

    return fake_runner


# ═══════════════════════════════════════════════════════════════════════════
# 회귀 1: smoke PASS → cleanup_summary 존재 + applied_count=0 (dry-run)
# ═══════════════════════════════════════════════════════════════════════════

def test_smoke_pass_triggers_cleanup_dry_run(tmp_path: Path) -> None:
    """smoke PASS → return dict 에 worktree_cleanup_summary 키 존재, apply=False, applied_count=0."""
    wt_path = str(tmp_path / "some-worktree")
    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    (events_dir / f"{FAKE_TASK_ID}.done.acked").touch()
    (events_dir / f"{FAKE_TASK_ID}.merge-done").touch()

    worktree_porcelain = (
        f"worktree {wt_path}\n"
        f"HEAD {FAKE_SHA40}\n"
        f"branch refs/heads/{FAKE_BRANCH}\n"
    )

    fake_runner = _make_smoke_pass_runner(worktree_porcelain=worktree_porcelain)
    runner = PostMergeSmokeRunner(
        subprocess_runner=fake_runner,
        capabilities_loader=lambda _: None,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    result = runner.run_post_merge_smoke(
        task_id=FAKE_TASK_ID,
        merge_commit=FAKE_SHA40,
        expected_files=["anu_v2/worktree_cleanup.py"],
    )

    assert result["outcome"] == "PASS"
    assert "worktree_cleanup_summary" in result
    summary = result["worktree_cleanup_summary"]
    assert summary is not None
    assert summary["apply"] is False
    assert summary["applied_count"] == 0


# ═══════════════════════════════════════════════════════════════════════════
# 회귀 2: medium #3 — cleanup_candidates >= 1 in dry-run 시 (가시성 회복 본질)
# ═══════════════════════════════════════════════════════════════════════════

def test_medium_3_dry_run_cleanup_candidates_visible_when_safety_1to5_pass(tmp_path: Path) -> None:
    """task-2550+1 medium #3 핵심 회귀:
       safety 1~5 모두 PASS 인 worktree 가 있을 때 dry-run 에서도
       cleanup_candidates >= 1 (가시성 회복).

    기존 BUG: `r.all_safe and not r.is_main and not r.dirty` 는
              dry-run 에서 항상 False → candidate 항상 0 (가시성 상실).
    수정 후: `is_safe_ignoring_apply(r)` 사용 → 1~5 PASS 시 candidate 1.
    """
    wt_path = str(tmp_path / "some-worktree-2550plus1")
    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    # safety 1, 3 PASS 위한 마커
    (events_dir / f"{FAKE_TASK_ID}.done.acked").touch()
    (events_dir / f"{FAKE_TASK_ID}.merge-done").touch()

    # safety 2: PR MERGED + headRefName strict match
    worktree_porcelain = (
        f"worktree {wt_path}\n"
        f"HEAD {FAKE_SHA40}\n"
        f"branch refs/heads/{FAKE_BRANCH}\n"
    )

    fake_runner = _make_smoke_pass_runner(worktree_porcelain=worktree_porcelain)
    runner = PostMergeSmokeRunner(
        subprocess_runner=fake_runner,
        capabilities_loader=lambda _: None,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    summary = runner.run_post_merge_worktree_cleanup_dry_run(task_id=FAKE_TASK_ID, apply=False)

    # 1 worktree 가 safety 1~5 모두 PASS → cleanup_candidates == 1
    assert summary["apply"] is False
    assert summary["total_worktrees"] == 1
    assert summary["cleanup_candidates"] == 1, (
        f"medium #3 회귀: dry-run 에서도 1~5 PASS 인 worktree 는 candidate 로 가시성 확보 — got "
        f"cleanup_candidates={summary['cleanup_candidates']} (results={summary['results']})"
    )
    # dry-run 이므로 실제 삭제 0
    assert summary["applied_count"] == 0


# ═══════════════════════════════════════════════════════════════════════════
# 회귀 3: cleanup_candidates dedup — safety 2 FAIL (unrelated PR) 면 candidate 0
# ═══════════════════════════════════════════════════════════════════════════

def test_medium_3_dry_run_cleanup_candidates_zero_when_safety_2_fails(tmp_path: Path) -> None:
    """safety 2 FAIL (PR state OPEN 또는 strict match 불가) → candidate 0."""
    wt_path = str(tmp_path / "some-worktree")
    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    (events_dir / f"{FAKE_TASK_ID}.done.acked").touch()
    (events_dir / f"{FAKE_TASK_ID}.merge-done").touch()

    worktree_porcelain = (
        f"worktree {wt_path}\n"
        f"HEAD {FAKE_SHA40}\n"
        f"branch refs/heads/{FAKE_BRANCH}\n"
    )

    # PR state = OPEN → safety 2 FAIL
    fake_runner = _make_smoke_pass_runner(
        worktree_porcelain=worktree_porcelain,
        pr_state="OPEN",
    )
    runner = PostMergeSmokeRunner(
        subprocess_runner=fake_runner,
        capabilities_loader=lambda _: None,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    summary = runner.run_post_merge_worktree_cleanup_dry_run(task_id=FAKE_TASK_ID, apply=False)
    assert summary["cleanup_candidates"] == 0, (
        f"safety 2 FAIL 시 candidate 는 0 이어야 함. got {summary['cleanup_candidates']}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# 회귀 4: smoke FAIL → cleanup 호출 X
# ═══════════════════════════════════════════════════════════════════════════

def test_smoke_fail_does_not_trigger_cleanup(tmp_path: Path) -> None:
    """smoke FAIL → worktree_cleanup_summary 키 없음 (또는 None)."""
    runner = PostMergeSmokeRunner(
        subprocess_runner=_make_smoke_fail_runner(),
        capabilities_loader=lambda _: None,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    result = runner.run_post_merge_smoke(
        task_id=FAKE_TASK_ID,
        merge_commit=FAKE_SHA40,
        expected_files=[],
    )

    assert result["outcome"] == "FAIL"
    cleanup_summary = result.get("worktree_cleanup_summary")
    assert cleanup_summary is None


# ═══════════════════════════════════════════════════════════════════════════
# 회귀 5: cleanup raise 해도 smoke PASS 결과 영향 X
# ═══════════════════════════════════════════════════════════════════════════

def test_smoke_pass_cleanup_failure_does_not_break_smoke_result(tmp_path: Path) -> None:
    """cleanup raise 해도 smoke PASS 결과는 PASS (try/except 보호)."""

    def fake_runner(args, **_):
        if "rev-parse" in args:
            return _proc(0, FAKE_MAIN_SHA + "\n", "")
        if "pytest" in " ".join(str(a) for a in args) or (args and "python3" in str(args[0])):
            return _proc(0, "ok", "")
        if "worktree" in args and "list" in args:
            raise RuntimeError("simulated cleanup failure")
        return _proc(0, "", "")

    runner = PostMergeSmokeRunner(
        subprocess_runner=fake_runner,
        capabilities_loader=lambda _: None,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    result = runner.run_post_merge_smoke(
        task_id=FAKE_TASK_ID,
        merge_commit=FAKE_SHA40,
        expected_files=[],
    )

    assert result["outcome"] == "PASS"
    assert result["exit_code"] == 0
    # cleanup 실패 → summary None
    assert result.get("worktree_cleanup_summary") is None


# ═══════════════════════════════════════════════════════════════════════════
# 회귀 6: dry-run schema — 필수 키 (task_id / apply / cleanup_candidates 등) 완전성
# ═══════════════════════════════════════════════════════════════════════════

def test_post_merge_worktree_cleanup_dry_run_schema(tmp_path: Path) -> None:
    """리턴 dict 필수 키 완전성 + 타입 검증."""
    wt_path = str(tmp_path / "some-worktree")
    worktree_porcelain = (
        f"worktree {wt_path}\n"
        f"HEAD {FAKE_SHA40}\n"
        f"branch refs/heads/{FAKE_BRANCH}\n"
    )
    fake_runner = _make_smoke_pass_runner(worktree_porcelain=worktree_porcelain)
    runner = PostMergeSmokeRunner(
        subprocess_runner=fake_runner,
        capabilities_loader=lambda _: None,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    result = runner.run_post_merge_worktree_cleanup_dry_run(task_id=FAKE_TASK_ID, apply=False)

    required = {
        "task_id", "apply", "total_worktrees", "cleanup_candidates",
        "applied_count", "skipped_count", "dirty_skipped", "main_protected",
        "ts", "results",
    }
    missing = required - set(result.keys())
    assert not missing, f"필수 키 누락: {missing}"
    assert isinstance(result["cleanup_candidates"], int)
    assert isinstance(result["apply"], bool)
    assert result["task_id"] == FAKE_TASK_ID
    assert result["apply"] is False
    assert result["applied_count"] == 0
    assert result["total_worktrees"] >= 0


# ═══════════════════════════════════════════════════════════════════════════
# 회귀 7: HIGH boundary 회귀 — smoke 통합 시점에서 task-25 strict 차단
# ═══════════════════════════════════════════════════════════════════════════

def test_high_boundary_integration_task25_does_not_match_task2550_in_smoke_cleanup(tmp_path: Path) -> None:
    """smoke 통합 시점에서 strict task_id 매칭 — task-25 검색 시 task-2550 PR 채택 X.

    cleanup_candidates 가 task-25 의 PR 미발견으로 0 이어야 함
    (BUG 였다면 task-2550 의 MERGED 가 task-25 에 매칭되어 candidate 1).
    """
    wt_path = str(tmp_path / "small-task-wt")
    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    (events_dir / "task-25.done.acked").touch()
    (events_dir / "task-25.merge-done").touch()

    worktree_porcelain = (
        f"worktree {wt_path}\n"
        f"HEAD {FAKE_SHA40}\n"
        f"branch refs/heads/task/task-25-dev3\n"  # task-25
    )

    # PR list: task-2550 MERGED + task-25 없음
    pr_response = json.dumps([
        {"number": 100, "state": "MERGED", "headRefName": "task/task-2550-dev5"},
    ])

    def fake_runner(args, **_):
        if "rev-parse" in args:
            return _proc(0, FAKE_MAIN_SHA + "\n", "")
        if "worktree" in args and "list" in args:
            return _proc(0, worktree_porcelain, "")
        if "status" in args and "--porcelain" in args:
            return _proc(0, "", "")
        if "pr" in args and "list" in args:
            return _proc(0, pr_response, "")
        if "merge-base" in args:
            return _proc(0, "", "")
        if "pgrep" in args:
            return _proc(1, "", "")
        if args and args[0] == "lsof":
            return _proc(1, "", "")
        return _proc(0, "", "")

    runner = PostMergeSmokeRunner(
        subprocess_runner=fake_runner,
        capabilities_loader=lambda _: None,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    summary = runner.run_post_merge_worktree_cleanup_dry_run(task_id="task-25", apply=False)
    # task-25 매칭 PR 이 list 에 없음 → safety 2 FAIL → candidate 0
    assert summary["cleanup_candidates"] == 0, (
        f"HIGH 통합 회귀: task-25 가 task-2550 PR 에 부분 일치 매칭 차단되어야 함. "
        f"got cleanup_candidates={summary['cleanup_candidates']}"
    )
