"""회귀 테스트 17건 — anu_v2.worktree_cleanup (task-2550).

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

⚠️ 테스트 코드 내 "ghp_faketoken123abc" 등 raw token placeholder는
   실제 토큰이 아닌 테스트 fake 값임 — leak detector 오탐 방지를 위해 명시.
"""

from __future__ import annotations

import json
import subprocess
import sys
from datetime import datetime, timezone
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.worktree_cleanup import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    DEFAULT_CHAT_ID,
    WorktreeCandidate,
    WorktreeCleanup,
)

# ─── 공용 상수 / 헬퍼 ─────────────────────────────────────────────────────────
FAKE_SHA = "a" * 40
FAKE_TASK_ID = "task-2550"
FAKE_BRANCH = "task/task-2550-dev5"
FAKE_WORKTREE_PATH = "/home/jay/workspace/.worktrees/task-2550-dev5"


def _fake_clock() -> datetime:
    """고정 시각 반환 — 테스트 결정론 보장."""
    return datetime(2026, 5, 11, 12, 0, 0, tzinfo=timezone.utc)


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


def _make_candidate(
    path: str = FAKE_WORKTREE_PATH,
    branch: str = FAKE_BRANCH,
    task_id: str | None = FAKE_TASK_ID,
    head_sha: str = FAKE_SHA,
) -> WorktreeCandidate:
    return WorktreeCandidate(path=path, branch=branch, task_id=task_id, head_sha=head_sha)


def _noop_runner(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
    """기본 fake subprocess runner — rc=0, no output. unused parameter는 명시적으로 del로 처리."""
    del _args, _kwargs
    return _make_completed_process(0)


# ─────────────────────────────────────────────────────────────────────────────
# Test 1: safety_1 done_acked — 마커 존재 → PASS
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_1_done_acked_pass(tmp_path: Path) -> None:
    """.done.acked 마커 존재 → passed=True."""
    # 마커 파일 생성
    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    (events_dir / f"{FAKE_TASK_ID}.done.acked").touch()

    cleanup = WorktreeCleanup(
        subprocess_runner=_noop_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_1_done_acked(FAKE_TASK_ID)

    assert result.condition == 1
    assert result.name == "done_acked"
    assert result.passed is True
    assert result.detail == "ok"


# ─────────────────────────────────────────────────────────────────────────────
# Test 2: safety_1 done_acked — 마커 없음 → FAIL
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_1_done_acked_fail(tmp_path: Path) -> None:
    """마커 없음 → passed=False."""
    cleanup = WorktreeCleanup(
        subprocess_runner=_noop_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_1_done_acked(FAKE_TASK_ID)

    assert result.condition == 1
    assert result.name == "done_acked"
    assert result.passed is False
    assert "missing" in result.detail


# ─────────────────────────────────────────────────────────────────────────────
# Test 3: safety_2 pr_merged — gh API mock PR state=MERGED → PASS
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_2_pr_merged_pass(tmp_path: Path) -> None:
    """gh API mock으로 PR state=MERGED 반환 → passed=True."""
    pr_response = json.dumps([
        {"number": 101, "state": "MERGED", "headRefName": FAKE_BRANCH}
    ])

    def fake_runner(args, **_):
        if "pr" in args and "list" in args:
            return _make_completed_process(0, pr_response, "")
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_2_pr_merged(FAKE_TASK_ID)

    assert result.condition == 2
    assert result.name == "pr_merged"
    assert result.passed is True
    assert "MERGED" in result.detail


# ─────────────────────────────────────────────────────────────────────────────
# Test 4: safety_2 pr_merged — gh API PR state=OPEN → FAIL
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_2_pr_merged_fail_open(tmp_path: Path) -> None:
    """gh API mock으로 PR state=OPEN 반환 → passed=False."""
    pr_response = json.dumps([
        {"number": 102, "state": "OPEN", "headRefName": FAKE_BRANCH}
    ])

    def fake_runner(args, **_):
        if "pr" in args and "list" in args:
            return _make_completed_process(0, pr_response, "")
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_2_pr_merged(FAKE_TASK_ID)

    assert result.condition == 2
    assert result.name == "pr_merged"
    assert result.passed is False
    assert "OPEN" in result.detail


# ─────────────────────────────────────────────────────────────────────────────
# Test 5: safety_3 merge_done — 마커 존재 → PASS
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_3_merge_done_pass(tmp_path: Path) -> None:
    """.merge-done 마커 존재 → passed=True."""
    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    (events_dir / f"{FAKE_TASK_ID}.merge-done").touch()

    cleanup = WorktreeCleanup(
        subprocess_runner=_noop_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_3_merge_done(FAKE_TASK_ID)

    assert result.condition == 3
    assert result.name == "merge_done"
    assert result.passed is True
    assert result.detail == "ok"


# ─────────────────────────────────────────────────────────────────────────────
# Test 6: safety_4 branch_in_main — rc=0 → PASS
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_4_branch_in_main_pass(tmp_path: Path) -> None:
    """git merge-base --is-ancestor rc=0 → passed=True."""

    def fake_runner(args, **_):
        if "merge-base" in args and "--is-ancestor" in args:
            return _make_completed_process(0, "", "")
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_4_branch_in_main(FAKE_BRANCH)

    assert result.condition == 4
    assert result.name == "branch_in_main"
    assert result.passed is True
    assert "ancestor" in result.detail


# ─────────────────────────────────────────────────────────────────────────────
# Test 7: safety_4 branch_in_main — rc=1 → FAIL
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_4_branch_in_main_fail(tmp_path: Path) -> None:
    """git merge-base --is-ancestor rc=1 → passed=False."""

    def fake_runner(args, **_):
        if "merge-base" in args and "--is-ancestor" in args:
            return _make_completed_process(1, "", "")
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_4_branch_in_main(FAKE_BRANCH)

    assert result.condition == 4
    assert result.name == "branch_in_main"
    assert result.passed is False
    assert "NOT ancestor" in result.detail or "rc=1" in result.detail


# ─────────────────────────────────────────────────────────────────────────────
# Test 8: safety_5 not_in_use — pgrep rc=1 (매치 없음) → PASS
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_5_not_in_use_pass(tmp_path: Path) -> None:
    """pgrep rc=1 (매치 없음) → passed=True."""

    def fake_runner(args, **_):
        if "pgrep" in args:
            return _make_completed_process(1, "", "")  # rc=1: 매치 없음
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_5_not_in_use(FAKE_WORKTREE_PATH)

    assert result.condition == 5
    assert result.name == "not_in_use"
    assert result.passed is True
    assert "no process" in result.detail


# ─────────────────────────────────────────────────────────────────────────────
# Test 9: safety_5 not_in_use — pgrep rc=0 (매치 있음) → FAIL
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_5_not_in_use_fail(tmp_path: Path) -> None:
    """pgrep rc=0 (매치 있음) → passed=False."""

    def fake_runner(args, **_):
        if "pgrep" in args:
            return _make_completed_process(0, "12345\n", "")  # rc=0: 매치 있음
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    result = cleanup.check_safety_5_not_in_use(FAKE_WORKTREE_PATH)

    assert result.condition == 5
    assert result.name == "not_in_use"
    assert result.passed is False
    assert "process" in result.detail


# ─────────────────────────────────────────────────────────────────────────────
# Test 10: safety_6 apply_explicit — apply=True → PASS; apply=False → FAIL
# ─────────────────────────────────────────────────────────────────────────────
def test_safety_6_apply_explicit(tmp_path: Path) -> None:
    """apply=True → passed=True; apply=False → passed=False."""
    cleanup = WorktreeCleanup(
        subprocess_runner=_noop_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    result_true = cleanup.check_safety_6_apply_explicit(True)
    assert result_true.condition == 6
    assert result_true.name == "apply_explicit"
    assert result_true.passed is True

    result_false = cleanup.check_safety_6_apply_explicit(False)
    assert result_false.condition == 6
    assert result_false.name == "apply_explicit"
    assert result_false.passed is False
    assert "dry-run" in result_false.detail


# ─────────────────────────────────────────────────────────────────────────────
# Test 11: dirty worktree → skipped=True, skip_reason="dirty", log 파일 생성
# ─────────────────────────────────────────────────────────────────────────────
def test_dirty_worktree_skipped(tmp_path: Path) -> None:
    """dirty=True인 worktree → CleanupResult.skipped=True, skip_reason 포함 "dirty", applied=False, log 파일 생성."""

    def fake_runner(args, **_):
        # git status --porcelain → dirty (비어있지 않은 output)
        if "status" in args and "--porcelain" in args:
            return _make_completed_process(0, "M  modified_file.py\n", "")
        # is_main_worktree 검사용 worktree path → NOT main
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )
    candidate = _make_candidate(path=str(tmp_path / "some-worktree"))

    result = cleanup.cleanup_worktree(candidate, apply=False)

    assert result.dirty is True
    assert result.skipped is True
    assert result.applied is False
    assert result.skip_reason is not None
    assert "dirty" in result.skip_reason.lower()

    # log 파일 생성 확인 (memory/events/worktree-cleanup-skipped-*.json)
    events_dir = tmp_path / "memory" / "events"
    log_files = list(events_dir.glob("worktree-cleanup-skipped-*.json"))
    assert len(log_files) >= 1, "dirty skip 시 log 파일이 생성되어야 함"

    # log 파일 내용 검증 (chat_id 포함)
    log_data = json.loads(log_files[0].read_text(encoding="utf-8"))
    assert log_data["chat_id"] == DEFAULT_CHAT_ID, f"chat_id가 {DEFAULT_CHAT_ID}이어야 함"
    assert log_data["task_id"] == FAKE_TASK_ID


# ─────────────────────────────────────────────────────────────────────────────
# Test 12: main worktree 절대 삭제 금지
# ─────────────────────────────────────────────────────────────────────────────
def test_main_worktree_never_deleted(tmp_path: Path) -> None:
    """workspace_root와 동일 path → is_main=True, applied=False, skip_reason에 'main worktree' 포함.

    추가: main 차단 이벤트가 회장 가시성 확보를 위해 log 파일에 박제됨을 검증.
    """
    cleanup = WorktreeCleanup(
        subprocess_runner=_noop_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    # candidate path = workspace_root (main worktree)
    candidate = _make_candidate(path=str(tmp_path))

    result = cleanup.cleanup_worktree(candidate, apply=True)  # apply=True여도

    assert result.is_main is True
    assert result.applied is False
    assert result.skipped is True
    assert result.skip_reason is not None
    assert "main worktree" in result.skip_reason.lower()

    # log 파일 박제 확인 (회장 가시성)
    events_dir = tmp_path / "memory" / "events"
    log_files = list(events_dir.glob("worktree-cleanup-skipped-*.json"))
    assert len(log_files) >= 1, "main 차단 시 log 파일이 박제되어야 함 (회장 가시성)"
    log_data = json.loads(log_files[0].read_text(encoding="utf-8"))
    assert log_data["reason"] == "main_worktree_protected"
    assert log_data["chat_id"] == DEFAULT_CHAT_ID


# ─────────────────────────────────────────────────────────────────────────────
# Test 13: dry-run (apply=False) — 모든 safety PASS여도 applied_count=0
# ─────────────────────────────────────────────────────────────────────────────
def test_dry_run_zero_actual_delete(tmp_path: Path) -> None:
    """apply=False, 모든 safety PASS여도 applied_count=0 어설션."""
    # 모든 안전 조건 PASS를 위한 마커 파일 생성
    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()

    pr_response = json.dumps([
        {"number": 103, "state": "MERGED", "headRefName": FAKE_BRANCH}
    ])

    def fake_runner(args, **_):
        if "status" in args and "--porcelain" in args:
            return _make_completed_process(0, "", "")  # clean (not dirty)
        if "pr" in args and "list" in args:
            return _make_completed_process(0, pr_response, "")
        if "merge-base" in args:
            return _make_completed_process(0, "", "")  # ancestor
        if "pgrep" in args:
            return _make_completed_process(1, "", "")  # not in use
        # git worktree list --porcelain
        if "worktree" in args and "list" in args:
            wt_path = str(tmp_path / "some-worktree")
            porcelain = (
                f"worktree {wt_path}\n"
                f"HEAD {FAKE_SHA}\n"
                f"branch refs/heads/{FAKE_BRANCH}\n"
            )
            return _make_completed_process(0, porcelain, "")
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    results = cleanup.cleanup_all_dry_run(apply=False)
    applied_count = sum(1 for r in results if r.applied)

    # dry-run이므로 실제 삭제 0건
    assert applied_count == 0, f"dry-run에서 applied_count가 0이어야 함 (got {applied_count})"


# ─────────────────────────────────────────────────────────────────────────────
# Test 14: apply=True + all 6 safety PASS → applied=True
# ─────────────────────────────────────────────────────────────────────────────
def test_apply_mode_with_all_safe_deletes(tmp_path: Path) -> None:
    """apply=True, 6대 safety 모두 PASS, mock subprocess 'git worktree remove' rc=0 → applied=True."""
    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()

    pr_response = json.dumps([
        {"number": 104, "state": "MERGED", "headRefName": FAKE_BRANCH}
    ])

    removed = {"called": False}

    def fake_runner(args, **_):
        if "status" in args and "--porcelain" in args:
            return _make_completed_process(0, "", "")  # clean
        if "pr" in args and "list" in args:
            return _make_completed_process(0, pr_response, "")
        if "merge-base" in args:
            return _make_completed_process(0, "", "")
        if "pgrep" in args:
            return _make_completed_process(1, "", "")  # not in use
        if "worktree" in args and "remove" in args:
            removed["called"] = True
            return _make_completed_process(0, "", "")  # 삭제 성공
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    # workspace_root가 아닌 별도 경로 사용 (is_main = False)
    candidate = _make_candidate(path=str(tmp_path / "some-worktree"))
    result = cleanup.cleanup_worktree(candidate, apply=True)

    assert result.all_safe is True
    assert result.applied is True
    assert result.skipped is False
    assert removed["called"] is True, "git worktree remove가 호출되어야 함"


# ─────────────────────────────────────────────────────────────────────────────
# Test 15: unmerged PR (safety_2 FAIL) → skipped=True, applied=False
# ─────────────────────────────────────────────────────────────────────────────
def test_unmerged_pr_skipped(tmp_path: Path) -> None:
    """safety_2 FAIL (PR OPEN) → cleanup_worktree all_safe=False, skipped=True, applied=False."""
    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()

    pr_response = json.dumps([
        {"number": 105, "state": "OPEN", "headRefName": FAKE_BRANCH}
    ])

    def fake_runner(args, **_):
        if "status" in args and "--porcelain" in args:
            return _make_completed_process(0, "", "")  # clean
        if "pr" in args and "list" in args:
            return _make_completed_process(0, pr_response, "")
        if "merge-base" in args:
            return _make_completed_process(0, "", "")
        if "pgrep" in args:
            return _make_completed_process(1, "", "")
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    candidate = _make_candidate(path=str(tmp_path / "some-worktree"))
    result = cleanup.cleanup_worktree(candidate, apply=True)

    assert result.all_safe is False
    assert result.skipped is True
    assert result.applied is False
    assert result.skip_reason is not None
    # PR OPEN → safety_2 failed
    failed_safety_names = [r.name for r in result.safety_results if not r.passed]
    assert "pr_merged" in failed_safety_names


# ─────────────────────────────────────────────────────────────────────────────
# Test 16: token raw 노출 금지 — stderr에 raw token → skip_reason에 ***MASKED***
# ─────────────────────────────────────────────────────────────────────────────
def test_token_not_leaked_in_skip_reason(tmp_path: Path) -> None:
    """subprocess error 시 raw token 'ghp_faketoken123abc'가 stderr에 있어도 skip_reason에는 ***MASKED***로 출력.

    ⚠️ 'ghp_faketoken123abc'는 테스트 fake 값 (실제 토큰 아님).
    """
    # "ghp_faketoken123abc" — 테스트 fake, 실제 토큰 아님
    fake_raw_token = "ghp_faketoken123abc"
    fake_stderr_with_token = f"error: authentication failed with token {fake_raw_token}"

    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()

    pr_response = json.dumps([
        {"number": 106, "state": "MERGED", "headRefName": FAKE_BRANCH}
    ])

    def fake_runner(args, **_):
        if "status" in args and "--porcelain" in args:
            return _make_completed_process(0, "", "")  # clean
        if "pr" in args and "list" in args:
            return _make_completed_process(0, pr_response, "")
        if "merge-base" in args:
            return _make_completed_process(0, "", "")
        if "pgrep" in args:
            return _make_completed_process(1, "", "")
        if "worktree" in args and "remove" in args:
            # 삭제 실패 — stderr에 raw token 포함
            return _make_completed_process(1, "", fake_stderr_with_token)
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    candidate = _make_candidate(path=str(tmp_path / "some-worktree"))
    result = cleanup.cleanup_worktree(candidate, apply=True)

    # skip_reason에 raw token이 없어야 함
    assert result.skip_reason is not None
    assert fake_raw_token not in result.skip_reason, (
        f"skip_reason에 raw token이 노출됨: {result.skip_reason}"
    )
    assert "***MASKED***" in result.skip_reason, (
        f"skip_reason에 ***MASKED*** 처리가 없음: {result.skip_reason}"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 17: enumerate_worktrees — git worktree list --porcelain 파싱 정상
# ─────────────────────────────────────────────────────────────────────────────
def test_enumerate_parses_worktree_list_porcelain(tmp_path: Path) -> None:
    """git worktree list --porcelain mock output parsing 정상."""
    wt_path_1 = "/home/jay/workspace"
    wt_path_2 = "/home/jay/workspace/.worktrees/task-2550-dev5"
    wt_path_3 = "/home/jay/workspace/.worktrees/task-2545-dev3"

    porcelain_output = (
        f"worktree {wt_path_1}\n"
        f"HEAD {FAKE_SHA}\n"
        "branch refs/heads/main\n"
        "\n"
        f"worktree {wt_path_2}\n"
        f"HEAD {'b' * 40}\n"
        f"branch refs/heads/task/task-2550-dev5\n"
        "\n"
        f"worktree {wt_path_3}\n"
        f"HEAD {'c' * 40}\n"
        f"branch refs/heads/task/task-2545-dev3\n"
    )

    def fake_runner(args, **_):
        if "worktree" in args and "list" in args and "--porcelain" in args:
            return _make_completed_process(0, porcelain_output, "")
        return _make_completed_process(0, "", "")

    cleanup = WorktreeCleanup(
        subprocess_runner=fake_runner,
        clock=_fake_clock,
        workspace_root=tmp_path,
    )

    candidates = cleanup.enumerate_worktrees()

    assert len(candidates) == 3, f"3개 worktree가 파싱되어야 함 (got {len(candidates)})"

    # path 파싱 검증
    paths = {c.path for c in candidates}
    assert wt_path_1 in paths
    assert wt_path_2 in paths
    assert wt_path_3 in paths

    # task_id 추출 검증
    task_map = {c.path: c.task_id for c in candidates}
    assert task_map[wt_path_2] == "task-2550"
    assert task_map[wt_path_3] == "task-2545"

    # SHA 검증
    sha_map = {c.path: c.head_sha for c in candidates}
    assert sha_map[wt_path_1] == FAKE_SHA
    assert sha_map[wt_path_2] == "b" * 40
    assert sha_map[wt_path_3] == "c" * 40
