"""tests/regression/test_cleanup_branch_failclosed.py — task-2471+1 자동화 회귀.

회장 명령 (C): merged branch cleanup 자동화 — fail-closed 4 조건.

1. state.current_state == "DONE"
2. state.evidence.pr_state == "MERGED"
3. mergeCommit ancestry (origin/main 포함)
4. .done 존재 + .g3-fail 부재 + .done.escalated 부재(or archived)

조건 미충족 시: 삭제 차단 + audit 기록 + exit 1.

격리 환경에서 ``cmd_cleanup_branch`` 직접 호출로 실제 git 호출 없이
조건 검증 자체의 fail-closed 동작을 검증한다.
"""
from __future__ import annotations

import argparse
import importlib.util
import json
import sys
from pathlib import Path
from typing import Any

import pytest

WORKSPACE = Path(__file__).resolve().parents[2]


def _load_taskctl(monkeypatch: pytest.MonkeyPatch, root: Path):
    """taskctl 모듈을 임시 WORKSPACE_ROOT 환경에서 격리 로드."""
    monkeypatch.setenv("WORKSPACE_ROOT", str(root))
    spec = importlib.util.spec_from_file_location(
        "taskctl_isolated_cleanup",
        str(WORKSPACE / "scripts" / "taskctl.py"),
    )
    assert spec and spec.loader
    mod = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = mod
    spec.loader.exec_module(mod)
    return mod


@pytest.fixture()
def isolated(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
    state_dir = tmp_path / ".tasks" / "state"
    evidence_dir = tmp_path / ".tasks" / "evidence"
    events_dir = tmp_path / "memory" / "events"
    audit_dir = tmp_path / "memory" / "orchestration-audit"
    for d in (state_dir, evidence_dir, events_dir, audit_dir):
        d.mkdir(parents=True, exist_ok=True)
    mod = _load_taskctl(monkeypatch, tmp_path)
    return {
        "tmp_path": tmp_path,
        "mod": mod,
        "state_dir": state_dir,
        "evidence_dir": evidence_dir,
        "events_dir": events_dir,
        "audit_dir": audit_dir,
    }


def _write_state(
    state_dir: Path,
    task_id: str,
    *,
    current_state: str,
    pr_state: str | None,
    merge_sha: str | None,
    branch: str = "task/task-99999-dev0",
    pr_number: int = 99999,
) -> None:
    """checksum 포함 state 직접 기록 (taskctl _save 우회)."""
    import hashlib

    payload: dict[str, Any] = {
        "task_id": task_id,
        "current_state": current_state,
        "transitions": [],
        "evidence": {
            "git_diff_sha": None,
            "changed_paths": [],
            "branch": branch,
            "pr_number": pr_number,
            "pr_state": pr_state,
            "merge_commit_sha": merge_sha,
            "ci_checks": {},
            "guard_sh_result": None,
            "qc_report_guard_result": None,
            "merge_timestamp": None,
            "exit_codes": {},
        },
        "human_approved": True,
        "bypass": {"used": False, "ts": None, "actor": None},
        "admin_override": {
            "used": False,
            "ts": None,
            "actor": None,
            "reason": None,
            "audit_log_offset": None,
        },
    }
    canonical = json.dumps(
        payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")
    )
    payload["_checksum"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
    (state_dir / f"{task_id}.json").write_text(
        json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
    )


def _make_args(task_id: str, **kwargs) -> argparse.Namespace:
    return argparse.Namespace(
        task_id=task_id,
        branch=kwargs.get("branch"),
        remote=kwargs.get("remote", False),
        dry_run=kwargs.get("dry_run", True),  # default dry-run for safety
        machine=kwargs.get("machine", True),
    )


def test_cleanup_blocked_when_state_not_done(isolated, capsys) -> None:
    """state != DONE → 차단."""
    task_id = "task-cleanup-001"
    _write_state(
        isolated["state_dir"],
        task_id,
        current_state="MERGED",  # not DONE
        pr_state="MERGED",
        merge_sha="abcd1234",
    )
    rc = isolated["mod"].cmd_cleanup_branch(_make_args(task_id))
    assert rc == 1
    out = capsys.readouterr().out
    rec = json.loads(out)
    assert rec["ok"] is False
    assert any("DONE" in r for r in rec["failed_reasons"])


def test_cleanup_blocked_when_pr_state_not_merged(isolated, capsys) -> None:
    """pr_state != MERGED → 차단."""
    task_id = "task-cleanup-002"
    _write_state(
        isolated["state_dir"],
        task_id,
        current_state="DONE",
        pr_state="OPEN",  # not MERGED
        merge_sha="abcd1234",
    )
    # required markers
    (isolated["events_dir"] / f"{task_id}.done").write_text("{}")
    rc = isolated["mod"].cmd_cleanup_branch(_make_args(task_id))
    assert rc == 1
    rec = json.loads(capsys.readouterr().out)
    assert rec["ok"] is False
    assert any("MERGED" in r or "OPEN" in r for r in rec["failed_reasons"])


def test_cleanup_blocked_when_done_marker_missing(isolated, capsys) -> None:
    """state DONE + pr MERGED 이지만 .done 마커 부재 → 차단."""
    task_id = "task-cleanup-003"
    _write_state(
        isolated["state_dir"],
        task_id,
        current_state="DONE",
        pr_state="MERGED",
        merge_sha="abcd1234",
    )
    # .done 부재
    rc = isolated["mod"].cmd_cleanup_branch(_make_args(task_id))
    assert rc == 1
    rec = json.loads(capsys.readouterr().out)
    assert rec["ok"] is False
    assert any(".done" in r for r in rec["failed_reasons"])


def test_cleanup_blocked_when_g3fail_present(isolated, capsys) -> None:
    """.g3-fail 마커 잔존 → 차단."""
    task_id = "task-cleanup-004"
    _write_state(
        isolated["state_dir"],
        task_id,
        current_state="DONE",
        pr_state="MERGED",
        merge_sha="abcd1234",
    )
    (isolated["events_dir"] / f"{task_id}.done").write_text("{}")
    (isolated["events_dir"] / f"{task_id}.g3-fail").write_text("{}")
    # evidence dir create
    (isolated["tmp_path"] / ".tasks" / "evidence" / task_id).mkdir(
        parents=True, exist_ok=True
    )
    rc = isolated["mod"].cmd_cleanup_branch(_make_args(task_id))
    assert rc == 1
    rec = json.loads(capsys.readouterr().out)
    assert rec["ok"] is False
    assert any("g3-fail" in r for r in rec["failed_reasons"])


def test_cleanup_blocked_when_escalated_present(isolated, capsys) -> None:
    """.done.escalated 잔존 (archive 안 됨) → 차단."""
    task_id = "task-cleanup-005"
    _write_state(
        isolated["state_dir"],
        task_id,
        current_state="DONE",
        pr_state="MERGED",
        merge_sha="abcd1234",
    )
    (isolated["events_dir"] / f"{task_id}.done").write_text("{}")
    (isolated["events_dir"] / f"{task_id}.done.escalated").write_text(
        json.dumps({"trigger": "x", "reason": "y"})
    )
    (isolated["tmp_path"] / ".tasks" / "evidence" / task_id).mkdir(
        parents=True, exist_ok=True
    )
    rc = isolated["mod"].cmd_cleanup_branch(_make_args(task_id))
    assert rc == 1
    rec = json.loads(capsys.readouterr().out)
    assert rec["ok"] is False
    assert any("escalated" in r for r in rec["failed_reasons"])


def test_cleanup_dry_run_passes_when_all_conditions_met(isolated, capsys) -> None:
    """모든 조건 충족 + dry-run → ok=True / 실제 삭제는 skip.

    NOTE: ancestry git 호출이 임시 dir에서 실패할 수 있어 ok=False인 것이
    정상 (fail-closed). 본 테스트는 그 한 가지가 아닌 다른 모든 조건이
    PASS인지 확인한다.
    """
    task_id = "task-cleanup-006"
    _write_state(
        isolated["state_dir"],
        task_id,
        current_state="DONE",
        pr_state="MERGED",
        merge_sha="abcd1234",
    )
    (isolated["events_dir"] / f"{task_id}.done").write_text("{}")
    (isolated["tmp_path"] / ".tasks" / "evidence" / task_id).mkdir(
        parents=True, exist_ok=True
    )
    rc = isolated["mod"].cmd_cleanup_branch(_make_args(task_id, dry_run=True))
    rec = json.loads(capsys.readouterr().out)
    # ancestry는 dummy SHA라 실패할 수 있음 — 그 외 4 check은 PASS여야 함
    state_done = rec["checks"]["state_current"]["ok"]
    pr_merged = rec["checks"]["pr_state_merged"]["ok"]
    evidence_dir_ok = rec["checks"]["evidence_dir"]["ok"]
    markers_ok = rec["checks"]["markers"]["ok"]
    assert state_done and pr_merged and evidence_dir_ok and markers_ok
    # ancestry는 dummy SHA로 fail (fail-closed 보호)
    assert rec["checks"]["ancestry"]["ok"] is False
    assert rec["deletion"]["dry_run"] is True
    # 어쨌든 fail-closed 동작 (ancestry fail → overall fail)
    assert rec["ok"] is False
    assert rc == 1
