"""tests/regression/test_done_hard_gate.py — task-2471 회귀 테스트.

토르가 commit 4795c5e2 에서 ``cmd_done`` 안에 통합한
``verify_done_preconditions`` (silent_corruption_guard) 의 hard-gate 동작을
영구 차단한다.

- 시그니처 검증 (3 check 호출 확인)
- 한 check 라도 FAIL 이면 ``ok=False``
- ``detail["failed_check"]`` 에 어느 check 가 실패했는지 명시

상위 ``cmd_done`` 자체는 다른 의존성이 많아 별도 e2e 테스트로 다룬다. 본 파일은
``verify_done_preconditions`` 의 boundary behavior 만 검증.

헤임달(개발2팀 테스터) 작성.
"""
from __future__ import annotations

import importlib.util
import inspect
import json
import sys
from pathlib import Path

import pytest

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


def _stub(*, ok: bool = True, reason: str = "ok", detail: dict | None = None):
    """Return a callable that ignores its args and returns the canned dict.

    Pyright in strict mode flags lambda parameters as unused; this helper
    encapsulates the cast and keeps the test bodies declarative.
    """
    payload = {"ok": ok, "reason": reason, "detail": detail or {}}

    def _inner(*args, **kwargs):  # noqa: ANN001, ARG001
        del args, kwargs
        return payload

    return _inner


@pytest.fixture(scope="module")
def scg():
    file_path = WORKSPACE / "utils" / "silent_corruption_guard.py"
    assert file_path.exists()
    spec = importlib.util.spec_from_file_location(
        "scg_done_hard_gate_test_alias", str(file_path)
    )
    assert spec is not None and spec.loader is not None
    mod = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = mod
    spec.loader.exec_module(mod)
    return mod


# ---------------------------------------------------------------------------
# 시그니처 검증
# ---------------------------------------------------------------------------


def test_verify_done_preconditions_signature(scg):
    """``verify_done_preconditions(pr_number, repo, *, base_branch="main",
    gh_cmd=None, cwd=None) -> dict`` 시그니처 보존."""
    sig = inspect.signature(scg.verify_done_preconditions)
    params = sig.parameters
    assert "pr_number" in params
    assert "repo" in params
    assert "base_branch" in params
    assert "gh_cmd" in params
    assert "cwd" in params
    # base_branch default 'main'
    assert params["base_branch"].default == "main"


def test_three_checks_callable(scg):
    """3 check 함수 모두 존재 + callable."""
    for name in (
        "check_pr_merged_at",
        "check_pr_merge_commit_oid",
        "check_origin_main_ancestry",
    ):
        assert hasattr(scg, name), f"{name} 미정의"
        assert callable(getattr(scg, name))


# ---------------------------------------------------------------------------
# verify_done_preconditions 가 3 check 를 모두 호출하는지 검증
# ---------------------------------------------------------------------------


def test_verify_done_invokes_three_checks(scg, monkeypatch):
    """verify_done_preconditions 가 mergedAt -> oid -> ancestry 순으로 모두 호출."""
    invoked: list[str] = []

    def _fake_merged_at(*_a, **_kw):
        del _a, _kw
        invoked.append("merged_at")
        return {"ok": True, "reason": "ok",
                "detail": {"mergedAt": "2026-05-07T00:00:00Z"}}

    def _fake_oid(*_a, **_kw):
        del _a, _kw
        invoked.append("merge_commit_oid")
        return {"ok": True, "reason": "ok",
                "detail": {"merge_commit_sha": "abc123"}}

    def _fake_ancestry(*_a, **_kw):
        del _a, _kw
        invoked.append("ancestry")
        return {"ok": True, "reason": "ok", "detail": {}}

    monkeypatch.setattr(scg, "check_pr_merged_at", _fake_merged_at)
    monkeypatch.setattr(scg, "check_pr_merge_commit_oid", _fake_oid)
    monkeypatch.setattr(scg, "check_origin_main_ancestry", _fake_ancestry)

    result = scg.verify_done_preconditions(1, "owner/repo")
    assert result["ok"] is True
    # 3 check 순서대로 호출
    assert invoked == ["merged_at", "merge_commit_oid", "ancestry"]


# ---------------------------------------------------------------------------
# 단일 check FAIL 시 verify_done_preconditions ok=False
# ---------------------------------------------------------------------------


def test_one_check_fail_blocks_done_at_merged_at(scg, monkeypatch):
    monkeypatch.setattr(
        scg, "check_pr_merged_at",
        _stub(ok=False, reason="mergedAt null"),
    )
    # 뒤 두 check 는 호출되지 않아야 하지만, 호출돼도 무방
    monkeypatch.setattr(
        scg, "check_pr_merge_commit_oid",
        _stub(detail={"merge_commit_sha": "y"}),
    )
    monkeypatch.setattr(
        scg, "check_origin_main_ancestry",
        _stub(),
    )

    result = scg.verify_done_preconditions(1, "owner/repo")
    assert result["ok"] is False
    assert result["detail"]["failed_check"] == "merged_at"


def test_one_check_fail_blocks_done_at_oid(scg, monkeypatch):
    monkeypatch.setattr(
        scg, "check_pr_merged_at",
        _stub(detail={"mergedAt": "T"}),
    )
    monkeypatch.setattr(
        scg, "check_pr_merge_commit_oid",
        _stub(ok=False, reason="oid null"),
    )
    monkeypatch.setattr(
        scg, "check_origin_main_ancestry",
        _stub(),
    )

    result = scg.verify_done_preconditions(2, "owner/repo")
    assert result["ok"] is False
    assert result["detail"]["failed_check"] == "merge_commit_oid"


def test_one_check_fail_blocks_done_at_ancestry(scg, monkeypatch):
    monkeypatch.setattr(
        scg, "check_pr_merged_at",
        _stub(detail={"mergedAt": "T"}),
    )
    monkeypatch.setattr(
        scg, "check_pr_merge_commit_oid",
        _stub(detail={"merge_commit_sha": "abc"}),
    )
    monkeypatch.setattr(
        scg, "check_origin_main_ancestry",
        _stub(ok=False, reason="not ancestor"),
    )

    result = scg.verify_done_preconditions(3, "owner/repo")
    assert result["ok"] is False
    assert result["detail"]["failed_check"] == "ancestry"


# ---------------------------------------------------------------------------
# detail 안에 어느 check 가 실패했는지 명시
# ---------------------------------------------------------------------------


def test_failure_detail_includes_checks_dict(scg, monkeypatch):
    """검증 실패 시 detail['checks'] 에 실행된 check 결과들이 보존."""
    monkeypatch.setattr(
        scg, "check_pr_merged_at",
        _stub(detail={"mergedAt": "T"}),
    )
    monkeypatch.setattr(
        scg, "check_pr_merge_commit_oid",
        _stub(ok=False, reason="oid missing"),
    )

    result = scg.verify_done_preconditions(4, "owner/repo")
    assert result["ok"] is False
    assert "checks" in result["detail"]
    checks = result["detail"]["checks"]
    # 첫 check 결과는 PASS 로 보존
    assert checks["merged_at"]["ok"] is True
    # 실패한 check 도 dict 안에 들어감
    assert checks["merge_commit_oid"]["ok"] is False


# ---------------------------------------------------------------------------
# 모든 check PASS 시 detail 의 핵심 키 확인
# ---------------------------------------------------------------------------


def test_success_detail_includes_merge_commit_and_merged_at(scg, monkeypatch):
    monkeypatch.setattr(
        scg, "check_pr_merged_at",
        _stub(detail={"mergedAt": "2026-05-07T00:00:00Z"}),
    )
    monkeypatch.setattr(
        scg, "check_pr_merge_commit_oid",
        _stub(detail={"merge_commit_sha": "deadbeef" * 5}),
    )
    monkeypatch.setattr(
        scg, "check_origin_main_ancestry",
        _stub(reason="ancestry ok"),
    )

    result = scg.verify_done_preconditions(5, "owner/repo", base_branch="main")
    assert result["ok"] is True
    assert result["detail"]["merge_commit_sha"] == "deadbeef" * 5
    assert result["detail"]["merged_at"] == "2026-05-07T00:00:00Z"
    assert result["detail"]["base_branch"] == "main"
    assert result["detail"]["pr_number"] == 5
    assert result["detail"]["repo"] == "owner/repo"


# ---------------------------------------------------------------------------
# Return type contract: ok / reason / detail 키 존재
# ---------------------------------------------------------------------------


def test_return_value_contract(scg, monkeypatch):
    """verify_done_preconditions 는 항상 {ok, reason, detail} 3 key 보장."""
    payload = json.dumps({"mergedAt": None, "mergeCommit": None})

    def _fake_run(_cmd, **_kwargs):  # noqa: ANN001
        del _cmd, _kwargs
        return (0, payload, "")

    monkeypatch.setattr(scg, "_run", _fake_run)
    monkeypatch.setattr(scg.time, "sleep", lambda _: None)

    result = scg.verify_done_preconditions(0, "owner/repo")
    assert set(result.keys()) >= {"ok", "reason", "detail"}
    assert isinstance(result["ok"], bool)
    assert isinstance(result["reason"], str)
    assert isinstance(result["detail"], dict)
