"""task-2554+2 §5 신규 fixture #4: PR head 변경 감지 시 즉시 FAILED + http_post 미발생.

회장 §명시 (2026-05-12): runner 호출 후 PR head 가 decision.current_head 와 다르면
``DecisionInvalidError`` (E_HEAD_MISMATCH) 즉시 발생 — http_post 0회.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from anu_v2.owner_trigger_audit import OwnerTriggerAudit
from anu_v2.owner_trigger_decision import DecisionInvalidError
from anu_v2.owner_trigger_only import OwnerTriggerOnly


_HEAD_A = "a" * 40
_HEAD_B = "b" * 40


def _write_decision(tmp_path: Path, *, head: str = _HEAD_A) -> Path:
    decision = {
        "schema": "anu_v2.owner_trigger_decision.v1",
        "task_id": "task-2554+2-test",
        "pr": 105,
        "current_head": head,
        "queue_head": True,
        "current_head_confirmed": True,
        "gemini_evidence_fresh": False,
        "nudge_count_for_pr_head": 0,
        "allowed_action": "POST_GEMINI_REVIEW_TRIGGER_COMMENT",
        "comment_body": "/gemini review",
        "allowed": True,
    }
    p = tmp_path / "decision.json"
    p.write_text(json.dumps(decision), encoding="utf-8")
    return p


def _build_module(tmp_path: Path):
    posts: list[dict] = []
    audit = OwnerTriggerAudit(tmp_path)

    def http_post(method, path, body, headers):
        posts.append({"method": method, "path": path, "body": dict(body)})
        return {"status": 201}

    mod = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=http_post,
        token_provider=lambda: "owner-token",
        audit=audit,
    )
    return mod, posts, audit


def test_decision_head_mismatch_actual_head_raises_head_mismatch(tmp_path):
    """decision.current_head=_HEAD_A 인데 actual head=_HEAD_B → DecisionInvalidError E_HEAD_MISMATCH."""
    decision_path = _write_decision(tmp_path, head=_HEAD_A)
    mod, posts, _ = _build_module(tmp_path)
    with pytest.raises(DecisionInvalidError) as exc_info:
        mod.trigger_gemini_review(
            decision_path=decision_path,
            owner="o",
            repo="r",
            current_head_actual=_HEAD_B,
        )
    assert exc_info.value.code == "E_HEAD_MISMATCH"
    # http_post 미호출 (fail-closed before lock entry)
    assert len(posts) == 0


def test_head_mismatch_does_not_record_pending_in_audit(tmp_path):
    """head mismatch 는 transaction 진입 전에 차단 → audit 에 PENDING 기록 0."""
    decision_path = _write_decision(tmp_path, head=_HEAD_A)
    mod, _, audit = _build_module(tmp_path)
    with pytest.raises(DecisionInvalidError):
        mod.trigger_gemini_review(
            decision_path=decision_path,
            owner="o",
            repo="r",
            current_head_actual=_HEAD_B,
        )
    rows = list(audit._iter_rows())
    assert rows == []


def test_invalid_actual_head_format_raises(tmp_path):
    """actual head 가 40-char hex 아니면 E_ACTUAL_HEAD_FORMAT."""
    decision_path = _write_decision(tmp_path, head=_HEAD_A)
    mod, _, _ = _build_module(tmp_path)
    with pytest.raises(DecisionInvalidError) as exc_info:
        mod.trigger_gemini_review(
            decision_path=decision_path,
            owner="o",
            repo="r",
            current_head_actual="short-head",
        )
    assert exc_info.value.code == "E_ACTUAL_HEAD_FORMAT"


def test_head_changed_subsequent_call_does_not_post_if_already_pending(tmp_path):
    """sequence: 1st call with head_A → PENDING(crash sim) → 2nd call with same head_A is dedupe.

    head 변경 (different actual head) 인 경우는 위 head_mismatch 로 처리.
    """
    decision_path = _write_decision(tmp_path, head=_HEAD_A)
    audit = OwnerTriggerAudit(tmp_path)

    class _SimulatedCrash(SystemExit):
        pass

    def crash_http_post(method, path, body, headers):
        raise _SimulatedCrash("post-PENDING crash")

    crashing_mod = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=crash_http_post,
        token_provider=lambda: "owner-token",
        audit=audit,
    )
    with pytest.raises(_SimulatedCrash):
        crashing_mod.trigger_gemini_review(
            decision_path=decision_path,
            owner="o",
            repo="r",
            current_head_actual=_HEAD_A,
        )
    # audit 에 PENDING 만 남음
    after_crash = list(audit._iter_rows())
    assert after_crash[0]["result"] == "PENDING"
    # 새 runner 가 같은 head 로 호출 → check_dedupe 가 PENDING 감지 → DEDUPED
    posts2: list[dict] = []

    def http_post(method, path, body, headers):
        posts2.append({"method": method})
        return {"status": 201}

    next_runner = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=http_post,
        token_provider=lambda: "owner-token",
        audit=audit,
    )
    r = next_runner.trigger_gemini_review(
        decision_path=decision_path,
        owner="o",
        repo="r",
        current_head_actual=_HEAD_A,
    )
    assert r.status == "DEDUPED"
    assert len(posts2) == 0  # http_post 미발생 (fail-closed)
