"""통합 회귀 테스트 5건 — PostMergeSmokeRunner + WorktreeCleanup 통합 (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
from typing import Callable

# 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.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"
FAKE_BRANCH = "task/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_smoke_pass_runner(
    *,
    worktree_porcelain: str = "",
    pr_state: str = "MERGED",
) -> Callable[..., subprocess.CompletedProcess]:
    """smoke PASS + worktree cleanup 지원 fake runner 생성."""
    pr_response = json.dumps([
        {"number": 200, "state": pr_state, "headRefName": FAKE_BRANCH}
    ])

    def fake_runner(args, **_):
        # git rev-parse origin/main → FAKE_MAIN_SHA
        if "rev-parse" in args:
            return _make_completed_process(0, FAKE_MAIN_SHA + "\n", "")
        # smoke command (python3 -m pytest ...) → PASS
        if "pytest" in " ".join(str(a) for a in args) or (args and "python3" in str(args[0])):
            return _make_completed_process(0, "smoke output ok", "")
        # git worktree list --porcelain
        if "worktree" in args and "list" in args and "--porcelain" in args:
            return _make_completed_process(0, worktree_porcelain, "")
        # git status --porcelain → clean
        if "status" in args and "--porcelain" in args:
            return _make_completed_process(0, "", "")
        # PR list
        if "pr" in args and "list" in args:
            return _make_completed_process(0, pr_response, "")
        # git merge-base --is-ancestor → ancestor
        if "merge-base" in args:
            return _make_completed_process(0, "", "")
        # pgrep → not in use
        if "pgrep" in args:
            return _make_completed_process(1, "", "")
        return _make_completed_process(0, "", "")

    return fake_runner


def _make_smoke_fail_runner() -> Callable[..., subprocess.CompletedProcess]:
    """smoke FAIL fake runner."""

    def fake_runner(args, **_):
        if "rev-parse" in args:
            return _make_completed_process(0, FAKE_MAIN_SHA + "\n", "")
        return _make_completed_process(1, "", "smoke failed")

    return fake_runner


# ─────────────────────────────────────────────────────────────────────────────
# Test 1: smoke PASS → worktree_cleanup_summary 키 존재, apply=False, applied_count=0
# ─────────────────────────────────────────────────────────────────────────────
def test_smoke_pass_triggers_cleanup_dry_run(tmp_path: Path) -> None:
    """smoke PASS → return dict에 worktree_cleanup_summary 키 존재 (None 아님), apply=False, applied_count=0."""
    # 단순 worktree 1개 (non-main) porcelain 설정
    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"

    # worktree_cleanup_summary 키 존재 + None 아님
    assert "worktree_cleanup_summary" in result, (
        "smoke PASS 시 worktree_cleanup_summary 키가 return dict에 있어야 함"
    )
    summary = result["worktree_cleanup_summary"]
    assert summary is not None, "worktree_cleanup_summary가 None이면 안 됨"

    # apply=False (dry-run)
    assert summary["apply"] is False

    # applied_count=0 (dry-run)
    assert summary["applied_count"] == 0, (
        f"dry-run에서 applied_count가 0이어야 함 (got {summary['applied_count']})"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 2: smoke FAIL → worktree_cleanup_summary 키 없음 (또는 None)
# ─────────────────────────────────────────────────────────────────────────────
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"

    # smoke FAIL → worktree_cleanup_summary 없음 또는 None
    cleanup_summary = result.get("worktree_cleanup_summary")
    assert cleanup_summary is None, (
        f"smoke FAIL 시 worktree_cleanup_summary가 None이어야 함 (got {cleanup_summary})"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 3: smoke PASS + enumerate 빈 list → total_worktrees=0, cleanup_candidates=0
# ─────────────────────────────────────────────────────────────────────────────
def test_smoke_pass_with_no_worktrees(tmp_path: Path) -> None:
    """enumerate가 빈 list 반환 → total_worktrees=0, cleanup_candidates=0."""
    # worktree_porcelain 빈 문자열 → enumerate_worktrees() = []
    fake_runner = _make_smoke_pass_runner(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=[],
    )

    assert result["outcome"] == "PASS"
    summary = result.get("worktree_cleanup_summary")
    # None이 아님 (dry-run 성공)
    assert summary is not None

    assert summary["total_worktrees"] == 0, (
        f"worktree가 없으면 total_worktrees=0 (got {summary['total_worktrees']})"
    )
    assert summary["cleanup_candidates"] == 0, (
        f"worktree가 없으면 cleanup_candidates=0 (got {summary['cleanup_candidates']})"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 4: cleanup raise해도 smoke PASS 결과 영향 X (try/except 어설션)
# ─────────────────────────────────────────────────────────────────────────────
def test_smoke_pass_cleanup_failure_does_not_break_smoke_result(tmp_path: Path) -> None:
    """cleanup 메서드가 raise해도 smoke PASS 결과 영향 X (try/except 어설션)."""

    call_count = {"n": 0}

    def fake_runner(args, **_):
        call_count["n"] += 1
        if "rev-parse" in args:
            return _make_completed_process(0, FAKE_MAIN_SHA + "\n", "")
        # smoke command → PASS
        if "pytest" in " ".join(str(a) for a in args) or (args and "python3" in str(args[0])):
            return _make_completed_process(0, "ok", "")
        # worktree list → raise RuntimeError (cleanup failure 시뮬)
        if "worktree" in args and "list" in args:
            raise RuntimeError("simulated cleanup failure")
        return _make_completed_process(0, "", "")

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

    # cleanup이 raise해도 smoke 결과는 PASS여야 함 (try/except 보호)
    result = runner.run_post_merge_smoke(
        task_id=FAKE_TASK_ID,
        merge_commit=FAKE_SHA40,
        expected_files=[],
    )

    assert result["outcome"] == "PASS", (
        f"cleanup 실패가 smoke PASS에 영향 주면 안 됨 (got {result['outcome']})"
    )
    assert result["exit_code"] == 0

    # worktree_cleanup_summary가 None (cleanup 실패) 이어도 smoke PASS
    # (try/except로 cleanup exception 잡힘)
    cleanup_summary = result.get("worktree_cleanup_summary")
    # cleanup 실패 → None (또는 key 없음)
    assert cleanup_summary is None, (
        "cleanup 실패 시 worktree_cleanup_summary는 None이어야 함 (try/except 보호)"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 5: run_post_merge_worktree_cleanup_dry_run 리턴 dict 스키마 완전성 검증
# ─────────────────────────────────────────────────────────────────────────────
def test_post_merge_worktree_cleanup_dry_run_returns_correct_schema(tmp_path: Path) -> None:
    """모든 키 존재 어설션 (task_id, apply, total_worktrees, cleanup_candidates,
    applied_count, skipped_count, dirty_skipped, main_protected, ts, results)."""

    # 단순 1개 worktree porcelain
    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"
    )

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

    def fake_runner(args, **_):
        if "worktree" in args and "list" in args and "--porcelain" in args:
            return _make_completed_process(0, worktree_porcelain, "")
        if "status" in args and "--porcelain" in args:
            return _make_completed_process(0, "", "")
        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, "", "")

    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_keys = {
        "task_id",
        "apply",
        "total_worktrees",
        "cleanup_candidates",
        "applied_count",
        "skipped_count",
        "dirty_skipped",
        "main_protected",
        "ts",
        "results",
    }
    missing_keys = required_keys - set(result.keys())
    assert not missing_keys, f"리턴 dict에 필수 키가 없음: {missing_keys}"

    # 타입 검증
    assert isinstance(result["task_id"], str), "task_id는 str이어야 함"
    assert isinstance(result["apply"], bool), "apply는 bool이어야 함"
    assert isinstance(result["total_worktrees"], int), "total_worktrees는 int이어야 함"
    assert isinstance(result["cleanup_candidates"], int), "cleanup_candidates는 int이어야 함"
    assert isinstance(result["applied_count"], int), "applied_count는 int이어야 함"
    assert isinstance(result["skipped_count"], int), "skipped_count는 int이어야 함"
    assert isinstance(result["dirty_skipped"], int), "dirty_skipped는 int이어야 함"
    assert isinstance(result["main_protected"], int), "main_protected는 int이어야 함"
    assert isinstance(result["ts"], str), "ts는 str이어야 함"
    assert isinstance(result["results"], list), "results는 list이어야 함"

    # 값 일관성 검증
    assert result["task_id"] == FAKE_TASK_ID
    assert result["apply"] is False
    # dry-run이므로 applied_count == 0
    assert result["applied_count"] == 0, (
        f"dry-run applied_count는 0이어야 함 (got {result['applied_count']})"
    )
    # total_worktrees >= 0
    assert result["total_worktrees"] >= 0

    # results 각 항목이 CleanupResult의 asdict() 형태인지 검증
    if result["results"]:
        first_result = result["results"][0]
        result_required_keys = {
            "worktree_path", "task_id", "safety_results", "all_safe",
            "dirty", "is_main", "applied", "skipped", "skip_reason", "ts",
        }
        missing_result_keys = result_required_keys - set(first_result.keys())
        assert not missing_result_keys, (
            f"results[0]에 필수 키가 없음: {missing_result_keys}"
        )
