"""task-2554+2 §5 신규 fixture #1: PR #105 bootstrap gap 재현.

회장 §명시 (2026-05-12): head 08f2d29c 에 대해 stale 1e907722 review 만 존재할 때
``detect_gemini_stale_on_head`` 가 ``GEMINI_STALE_ON_HEAD`` 판정을 내려야 하며
``emit_owner_trigger_decision`` 이 OWNER_TRIGGER_REQUIRED 결정 파일을 생성해야 한다.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from anu_v2.merge_queue_executor import (
    GEMINI_STALE_ON_HEAD,
    OWNER_TRIGGER_REQUIRED,
    MergeQueueExecutor,
    PRMeta,
)


_PR_105_HEAD = "08f2d29ccb149fd721722e69909e45ceaebc71ba"
_STALE_REVIEW_COMMIT_ID = "1e907722167fd1e0fe328c1196af76c049644b27"


def _build_executor(tmp_path: Path) -> MergeQueueExecutor:
    """안전한 mock callable 로 executor 생성."""

    def gh(args, env):  # pragma: no cover — 본 fixture 는 evaluate gate 호출 0
        raise NotImplementedError("gh runner not used in this fixture")

    def git(args):  # pragma: no cover
        raise NotImplementedError("git runner not used")

    def pytest_runner(paths):  # pragma: no cover
        return 0

    def audit_writer(payload):  # pragma: no cover
        return None

    return MergeQueueExecutor(
        gh_runner=gh,
        git_runner=git,
        pytest_runner=pytest_runner,
        audit_writer=audit_writer,
        task_md_root=tmp_path,
    )


def _pr(head: str = _PR_105_HEAD, number: int = 105) -> PRMeta:
    return PRMeta(
        number=number,
        head_sha=head,
        head_ref=f"task/task-2554+1-dev5",
        base_ref="main",
        changed_files=(),
        ci_required_all_success=True,
        gemini_status="GEMINI_UNRESOLVED",
        merge_state_status="BLOCKED",
        queue_predecessors_open=0,
    )


def test_pr105_head_vs_stale_review_detects_gemini_stale_on_head(tmp_path):
    """PR #105 head=08f2d29c vs stale 1e907722 review → GEMINI_STALE_ON_HEAD."""
    executor = _build_executor(tmp_path)
    outcome = executor.detect_gemini_stale_on_head(
        pr=_pr(),
        gemini_review_commit_id=_STALE_REVIEW_COMMIT_ID,
    )
    assert outcome.decision == GEMINI_STALE_ON_HEAD
    assert outcome.extra["head"] == _PR_105_HEAD
    assert outcome.extra["gemini_review_commit_id"] == _STALE_REVIEW_COMMIT_ID


def test_emit_owner_trigger_decision_creates_v1_schema_and_marker(tmp_path):
    """OWNER_TRIGGER_REQUIRED decision JSON v1 + requested marker 생성."""
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    decision = executor.emit_owner_trigger_decision(
        task_id="task-2554+2",
        pr=_pr(),
        decision_dir=decision_dir,
    )
    # decision schema 1:1
    assert decision["schema"] == "anu_v2.owner_trigger_decision.v1"
    assert decision["pr"] == 105
    assert decision["current_head"] == _PR_105_HEAD
    assert decision["allowed_action"] == "POST_GEMINI_REVIEW_TRIGGER_COMMENT"
    assert decision["comment_body"] == "/gemini review"
    assert decision["allowed"] is True
    assert decision["gemini_evidence_fresh"] is False
    assert decision["nudge_count_for_pr_head"] == 0
    # decision file written
    decision_path = decision_dir / "task-2554+2.owner_trigger_decision.json"
    assert decision_path.exists()
    saved = json.loads(decision_path.read_text(encoding="utf-8"))
    assert saved == decision
    # marker file
    marker_path = decision_dir / "task-2554+2.owner-trigger.requested"
    assert marker_path.exists()
    marker_data = json.loads(marker_path.read_text(encoding="utf-8"))
    assert marker_data["pr"] == 105
    assert marker_data["head"] == _PR_105_HEAD
    assert marker_data["decision_code"] == OWNER_TRIGGER_REQUIRED


def test_fresh_review_on_head_does_not_trigger_stale_decision(tmp_path):
    """head == gemini review commit_id 일 때 AUTO_MERGE_ALLOWED (stale 아님)."""
    executor = _build_executor(tmp_path)
    outcome = executor.detect_gemini_stale_on_head(
        pr=_pr(),
        gemini_review_commit_id=_PR_105_HEAD,
    )
    assert outcome.passed
    assert outcome.reason == "gemini_review_fresh_on_head"


def test_missing_gemini_review_yields_first_review_pending(tmp_path):
    """gemini_review_commit_id=None → first-review-pending (외부 자동 1st 대기)."""
    executor = _build_executor(tmp_path)
    outcome = executor.detect_gemini_stale_on_head(
        pr=_pr(),
        gemini_review_commit_id=None,
    )
    assert not outcome.passed
    assert outcome.reason == "gemini_first_review_pending"


def test_emit_decision_rejects_invalid_pr_head_sha(tmp_path):
    """pr.head_sha 가 40-char hex 아니면 ValueError fail-closed."""
    executor = _build_executor(tmp_path)
    bad_pr = PRMeta(
        number=105,
        head_sha="not-a-valid-sha",
        head_ref="task/task-2554+1-dev5",
        base_ref="main",
        changed_files=(),
        ci_required_all_success=True,
        gemini_status="GEMINI_UNRESOLVED",
        merge_state_status="BLOCKED",
        queue_predecessors_open=0,
    )
    with pytest.raises(ValueError, match="40-char"):
        executor.emit_owner_trigger_decision(
            task_id="task-2554+2",
            pr=bad_pr,
            decision_dir=tmp_path,
        )
