"""task-2554+2 §5 신규 fixture #6: post 후 24h+ fresh 미도착 시 marker + ESCALATED + auto-resume X.

회장 §명시 (2026-05-12) §3:
  - owner trigger POSTED 후 fresh Gemini review (commit_id == PR head) 가 24h 안 도착하면
    ``record_no_fresh_evidence_after_post`` 가 ``<task>.posted-but-no-fresh-evidence`` marker 생성.
  - decision_code = POSTED_BUT_NO_FRESH_EVIDENCE + critical_code = CRITICAL_BLOCK_OVERRIDE.
  - executor 는 OWNER_DECISION_REQUIRED 로 escalate, auto-resume 안 함.

또한 §3 detect_fresh_gemini_review 가 fresh 도착 시 GEMINI_FRESH_DETECTED + AUTO_MERGE_ALLOWED.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from anu_v2.merge_queue_executor import (
    AUTO_MERGE_ALLOWED,
    BLOCKED_WITH_REASON,
    CRITICAL_BLOCK_OVERRIDE,
    GEMINI_FRESH_DETECTED,
    OWNER_TRIGGER_FAILED,
    OWNER_TRIGGER_POSTED,
    POSTED_BUT_NO_FRESH_EVIDENCE,
    MergeQueueExecutor,
    PRMeta,
)


_PR_HEAD = "a" * 40
_DIFFERENT_HEAD = "b" * 40


def _build_executor(tmp_path: Path) -> MergeQueueExecutor:
    def gh(args, env):  # pragma: no cover
        raise NotImplementedError

    def git(args):  # pragma: no cover
        raise NotImplementedError

    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_HEAD, number: int = 105) -> PRMeta:
    return PRMeta(
        number=number,
        head_sha=head,
        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,
    )


# ─── posted marker / failed marker 생성 ──────────────────────────────────────


def test_record_posted_marker_creates_owner_trigger_posted_file(tmp_path):
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    marker = executor.record_owner_trigger_outcome(
        task_id="task-2554+2",
        pr=_pr(),
        outcome_code=OWNER_TRIGGER_POSTED,
        decision_dir=decision_dir,
        extra={"audit_token_hash_prefix": "abcd1234"},
    )
    assert marker.name == "task-2554+2.owner-trigger.posted"
    payload = json.loads(marker.read_text(encoding="utf-8"))
    assert payload["pr"] == 105
    assert payload["head"] == _PR_HEAD
    assert payload["outcome_code"] == OWNER_TRIGGER_POSTED
    assert payload["extra"]["audit_token_hash_prefix"] == "abcd1234"


def test_record_failed_marker_creates_owner_trigger_failed_file(tmp_path):
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    marker = executor.record_owner_trigger_outcome(
        task_id="task-2554+2",
        pr=_pr(),
        outcome_code=OWNER_TRIGGER_FAILED,
        decision_dir=decision_dir,
        extra={"error_code": "HTTP_POST_FAIL"},
    )
    assert marker.name == "task-2554+2.owner-trigger.failed"
    payload = json.loads(marker.read_text(encoding="utf-8"))
    assert payload["outcome_code"] == OWNER_TRIGGER_FAILED
    assert payload["extra"]["error_code"] == "HTTP_POST_FAIL"


def test_record_outcome_rejects_unknown_code(tmp_path):
    executor = _build_executor(tmp_path)
    with pytest.raises(ValueError, match="OWNER_TRIGGER_POSTED"):
        executor.record_owner_trigger_outcome(
            task_id="task-2554+2",
            pr=_pr(),
            outcome_code="SOMETHING_ELSE",
            decision_dir=tmp_path,
        )


# ─── fresh review detection ──────────────────────────────────────────────────


def test_detect_fresh_review_when_commit_id_matches_head(tmp_path):
    executor = _build_executor(tmp_path)
    outcome = executor.detect_fresh_gemini_review(
        pr=_pr(),
        latest_gemini_review_commit_id=_PR_HEAD,
    )
    assert outcome.decision == AUTO_MERGE_ALLOWED
    assert outcome.reason == GEMINI_FRESH_DETECTED


def test_detect_fresh_review_when_commit_id_does_not_match_head(tmp_path):
    executor = _build_executor(tmp_path)
    outcome = executor.detect_fresh_gemini_review(
        pr=_pr(),
        latest_gemini_review_commit_id=_DIFFERENT_HEAD,
    )
    assert outcome.decision == BLOCKED_WITH_REASON
    assert outcome.reason == "gemini_fresh_review_not_yet"


# ─── 24h+ fresh 미도착 marker ───────────────────────────────────────────────


def test_no_fresh_evidence_after_24h_creates_marker_and_blocks(tmp_path):
    """24h+ 경과 + fresh 미도착 → posted-but-no-fresh-evidence marker + ESCALATED."""
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    outcome = executor.record_no_fresh_evidence_after_post(
        task_id="task-2554+2",
        pr=_pr(),
        elapsed_seconds=25 * 3600,  # 25h
        threshold_seconds=24 * 3600,
        decision_dir=decision_dir,
    )
    assert outcome.decision == BLOCKED_WITH_REASON
    assert outcome.reason == POSTED_BUT_NO_FRESH_EVIDENCE
    assert outcome.critical_code == CRITICAL_BLOCK_OVERRIDE
    # auto-resume 안 함을 보장 — outcome.passed == False
    assert not outcome.passed
    # marker 파일 확인
    marker = decision_dir / "task-2554+2.posted-but-no-fresh-evidence"
    assert marker.exists()
    payload = json.loads(marker.read_text(encoding="utf-8"))
    assert payload["decision_code"] == POSTED_BUT_NO_FRESH_EVIDENCE
    assert payload["escalation"] == "OWNER_DECISION_REQUIRED"
    assert payload["elapsed_seconds"] == 25 * 3600


def test_within_24h_grace_period_does_not_create_marker(tmp_path):
    """24h 안 → grace period — marker 미생성, BLOCKED with grace reason."""
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    outcome = executor.record_no_fresh_evidence_after_post(
        task_id="task-2554+2",
        pr=_pr(),
        elapsed_seconds=12 * 3600,
        threshold_seconds=24 * 3600,
        decision_dir=decision_dir,
    )
    assert outcome.reason == "within_grace_period"
    # marker 미생성
    marker = decision_dir / "task-2554+2.posted-but-no-fresh-evidence"
    assert not marker.exists()


def test_auto_resume_not_triggered_after_escalation_marker(tmp_path):
    """escalation marker 가 생성된 후에도 fresh 미도착이면 계속 BLOCKED."""
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    # 1st: marker 생성
    executor.record_no_fresh_evidence_after_post(
        task_id="task-2554+2",
        pr=_pr(),
        elapsed_seconds=25 * 3600,
        threshold_seconds=24 * 3600,
        decision_dir=decision_dir,
    )
    # 2nd: 동일 호출 — 동일 escalation 응답 (auto-resume 0)
    outcome = executor.record_no_fresh_evidence_after_post(
        task_id="task-2554+2",
        pr=_pr(),
        elapsed_seconds=30 * 3600,
        threshold_seconds=24 * 3600,
        decision_dir=decision_dir,
    )
    assert outcome.reason == POSTED_BUT_NO_FRESH_EVIDENCE
    assert outcome.critical_code == CRITICAL_BLOCK_OVERRIDE


# ─── gemini-fresh-detected marker + orchestration ────────────────────────────


def test_mark_gemini_fresh_detected_creates_marker_file(tmp_path):
    """fresh 도착 시 ``.gemini-fresh-detected`` marker 생성."""
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    marker = executor.mark_gemini_fresh_detected(
        task_id="task-2554+2",
        pr=_pr(),
        gemini_review_commit_id=_PR_HEAD,
        decision_dir=decision_dir,
    )
    assert marker.name == "task-2554+2.gemini-fresh-detected"
    payload = json.loads(marker.read_text(encoding="utf-8"))
    assert payload["pr"] == 105
    assert payload["head"] == _PR_HEAD
    assert payload["gemini_review_commit_id"] == _PR_HEAD
    assert payload["decision_code"] == GEMINI_FRESH_DETECTED
    assert payload["auto_resume"] is True


def test_mark_fresh_rejects_when_commit_id_not_match_head(tmp_path):
    """head != commit_id 인데 fresh marker 생성 시도 → ValueError fail-closed."""
    executor = _build_executor(tmp_path)
    with pytest.raises(ValueError, match="must match"):
        executor.mark_gemini_fresh_detected(
            task_id="task-2554+2",
            pr=_pr(head=_PR_HEAD),
            gemini_review_commit_id=_DIFFERENT_HEAD,
            decision_dir=tmp_path,
        )


def test_orchestrate_owner_trigger_for_stale_pr_full_path(tmp_path):
    """orchestration: stale 감지 → decision 생성 → runner 호출 (POSTED) → posted marker."""
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    runner_calls: list[Path] = []

    def runner(decision_path):
        runner_calls.append(decision_path)
        return "POSTED"

    outcome = executor.orchestrate_owner_trigger_for_stale_pr(
        task_id="task-2554+2",
        pr=_pr(),
        stale_gemini_review_commit_id="1" * 40,  # different from PR head → stale
        owner_trigger_runner=runner,
        decision_dir=decision_dir,
    )
    assert outcome.reason == OWNER_TRIGGER_POSTED
    assert outcome.passed
    # runner 호출 1회
    assert len(runner_calls) == 1
    # decision.json + requested + posted 3 파일
    assert (decision_dir / "task-2554+2.owner_trigger_decision.json").exists()
    assert (decision_dir / "task-2554+2.owner-trigger.requested").exists()
    assert (decision_dir / "task-2554+2.owner-trigger.posted").exists()


def test_orchestrate_when_not_stale_passes_through(tmp_path):
    """head == commit_id 이면 stale 아님 — runner 호출 0."""
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"
    runner_calls: list[Path] = []

    def runner(decision_path):
        runner_calls.append(decision_path)
        return "POSTED"

    outcome = executor.orchestrate_owner_trigger_for_stale_pr(
        task_id="task-2554+2",
        pr=_pr(),
        stale_gemini_review_commit_id=_PR_HEAD,  # fresh
        owner_trigger_runner=runner,
        decision_dir=decision_dir,
    )
    assert outcome.reason == "gemini_review_fresh_on_head"
    assert outcome.passed
    assert len(runner_calls) == 0  # runner 호출 X


def test_orchestrate_when_runner_returns_failed_records_failed_marker(tmp_path):
    """runner FAILED → owner-trigger.failed marker + BLOCKED critical."""
    executor = _build_executor(tmp_path)
    decision_dir = tmp_path / "memory" / "events"

    def runner(decision_path):
        return "FAILED"

    outcome = executor.orchestrate_owner_trigger_for_stale_pr(
        task_id="task-2554+2",
        pr=_pr(),
        stale_gemini_review_commit_id="1" * 40,
        owner_trigger_runner=runner,
        decision_dir=decision_dir,
    )
    assert outcome.reason == OWNER_TRIGGER_FAILED
    assert outcome.critical_code == "BLOCK_OVERRIDE_REQUIRED_OR_INSUFFICIENT_REASON"
    assert (decision_dir / "task-2554+2.owner-trigger.failed").exists()
