"""tests.regression.test_owner_trigger_success_2553 — fixture 2 + 3 + 4 (task-2553).

회장 §명시 fixture:
  2. **owner trigger success** — OWNER PAT 댓글이 Gemini fresh review 도착 시뮬레이션
  3. **duplicate nudge blocked** — 같은 PR/head 2회 trigger 시 2회차 fail-fast
  4. **update-branch stale reset** — 새 head 생기면 기존 trigger 무효, 새 trigger 가능

본 regression 은 `default_audit_writer` 를 사용해 jsonl 박제까지 검증하며, dedupe
회로가 실제 파일 기반으로 동작함을 확인한다. mock fixture 로 audit_log_path 는
tmp_path 사용.

본 모듈은 `anu_v2.owner_trigger_pat` 를 **수정하지 않고** behavioral 검증만 수행한다.
"""

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path
from typing import Any, Mapping, Sequence

import pytest

# workspace root → sys.path
_WORKSPACE_ROOT = Path(__file__).resolve().parents[2]
if str(_WORKSPACE_ROOT) not in sys.path:
    sys.path.insert(0, str(_WORKSPACE_ROOT))

from anu_v2.owner_trigger_pat import (  # noqa: E402
    ALLOWED_COMMENT_BODY,
    DECISION_PASS,
    DECISION_REJECT,
    EVIDENCE_MISSING_FOR_CURRENT_HEAD,
    OUTCOME_OK,
    OUTCOME_REJECTED,
    OWNER_PAT_ENV_NAME,
    OwnerTriggerPat,
    default_audit_writer,
    write_decision_json,
)


# ─── helpers ────────────────────────────────────────────────────────────────
def _cp(returncode: int = 0, stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess:
    return subprocess.CompletedProcess(args=[], returncode=returncode, stdout=stdout, stderr=stderr)


_FAKE_OWNER_PAT = "ghp_TEST_FAKE_12345"
_FIXED_TS = "2026-05-11T12:00:00+00:00"


@pytest.fixture
def gh_calls() -> list[dict[str, Any]]:
    return []


@pytest.fixture
def audit_calls() -> list[dict[str, Any]]:
    return []


@pytest.fixture
def decision_calls() -> list[dict[str, Any]]:
    return []


def _make_trigger(
    gh_calls: list[dict[str, Any]],
    audit_calls: list[dict[str, Any]],
    decision_calls: list[dict[str, Any]],
    audit_log_path: Path,
) -> OwnerTriggerPat:
    """jsonl 박제까지 수행하는 trigger 인스턴스 생성.

    dedupe 회로 검증을 위해 audit_writer 가 실제 jsonl 1줄 append 도 수행.
    """
    file_writer = default_audit_writer(audit_log_path)

    def gh_runner(args: Sequence[str], env: Mapping[str, str]) -> subprocess.CompletedProcess:
        gh_calls.append({"args": list(args), "env": dict(env or {})})
        return _cp()

    def audit_writer(record: Mapping[str, Any]) -> None:
        audit_calls.append(dict(record))
        # 실제 파일에도 박제 → 다음 trigger 가 dedupe 검사 시 발견 가능.
        file_writer(record)

    def decision_writer(payload: Mapping[str, Any], path: Path) -> None:
        decision_calls.append({"payload": dict(payload), "path": str(path)})
        write_decision_json(payload, path)

    return OwnerTriggerPat(
        gh_runner=gh_runner,
        audit_writer=audit_writer,
        decision_writer=decision_writer,
        owner="jeon-jonghyuk",
        repo="taskctl-anu",
        clock=lambda: _FIXED_TS,
    )


# ─── fixture 2: owner trigger success — fresh review ─────────────────────────
def test_owner_trigger_success_fresh_review(
    tmp_path: Path,
    gh_calls: list[dict[str, Any]],
    audit_calls: list[dict[str, Any]],
    decision_calls: list[dict[str, Any]],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """OWNER PAT 환경에서 `/gemini review` 댓글 → gh_runner 1회 호출 + outcome=ok.

    회장 §명시 fixture 2 본질. 호출 args 가 정확히 issue comments POST 1 endpoint 이며
    body 가 `/gemini review` strict equality 임을 검증.
    """
    monkeypatch.setenv(OWNER_PAT_ENV_NAME, _FAKE_OWNER_PAT)
    audit_path = tmp_path / "owner_trigger_audit.jsonl"
    decision_path = tmp_path / "owner_trigger_decision.json"

    trigger = _make_trigger(gh_calls, audit_calls, decision_calls, audit_path)

    out = trigger.trigger_gemini_review(
        pr_number=81,
        head_sha="fresh001",
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=audit_path,
        decision_json_path=decision_path,
    )

    assert out.outcome == OUTCOME_OK
    assert out.decision_path == str(decision_path)
    assert len(gh_calls) == 1

    # endpoint + body 박제
    call = gh_calls[0]
    assert call["args"] == [
        "api", "-X", "POST",
        "/repos/jeon-jonghyuk/taskctl-anu/issues/81/comments",
        "-f", f"body={ALLOWED_COMMENT_BODY}",
    ]
    # OWNER PAT 이 env 에 주입됨
    assert call["env"]["GH_TOKEN"] == _FAKE_OWNER_PAT
    assert call["env"]["GITHUB_TOKEN"] == _FAKE_OWNER_PAT

    # decision PASS 박제
    decision_data = json.loads(decision_path.read_text(encoding="utf-8"))
    assert decision_data["decision"] == DECISION_PASS
    assert decision_data["pr_number"] == 81
    assert decision_data["dedupe_key"] == "81#fresh001"

    # audit 박제 + token raw 0
    assert len(audit_calls) == 1
    record = audit_calls[0]
    assert record["outcome"] == OUTCOME_OK
    assert record["token_present"] is True
    assert len(record["token_hash"]) == 12
    assert _FAKE_OWNER_PAT not in json.dumps(record)


# ─── fixture 3: duplicate nudge blocked ──────────────────────────────────────
def test_duplicate_nudge_blocked(
    tmp_path: Path,
    gh_calls: list[dict[str, Any]],
    audit_calls: list[dict[str, Any]],
    decision_calls: list[dict[str, Any]],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """같은 (pr, head_sha) 2회 trigger 시 2회차 outcome=rejected, reason 에 DUPLICATE 키워드.

    회장 §명시 fixture 3 + 6 허용 조건 #5: PR/head당 1회 only.
    """
    monkeypatch.setenv(OWNER_PAT_ENV_NAME, _FAKE_OWNER_PAT)
    audit_path = tmp_path / "audit.jsonl"
    decision_path = tmp_path / "decision.json"

    trigger = _make_trigger(gh_calls, audit_calls, decision_calls, audit_path)

    # 1회차 — OK
    out1 = trigger.trigger_gemini_review(
        pr_number=200,
        head_sha="samehead",
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=audit_path,
        decision_json_path=decision_path,
    )
    assert out1.outcome == OUTCOME_OK
    assert len(gh_calls) == 1

    # 2회차 — REJECTED (dedupe)
    out2 = trigger.trigger_gemini_review(
        pr_number=200,
        head_sha="samehead",
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=audit_path,
        decision_json_path=decision_path,
    )
    assert out2.outcome == OUTCOME_REJECTED
    # reason 에 DUPLICATE 또는 DEDUPE 키워드 포함 (대소문자 무관)
    reason_upper = out2.reason.upper()
    assert ("DUPLICATE" in reason_upper) or ("DEDUPE" in reason_upper)
    # gh 호출은 여전히 1 (2회차에서 추가 호출 X)
    assert len(gh_calls) == 1

    # 2회차 decision JSON 도 REJECT 로 박제됨 (audit trail 유지)
    decision_data = json.loads(decision_path.read_text(encoding="utf-8"))
    assert decision_data["decision"] == DECISION_REJECT


# ─── fixture 4: update-branch stale reset ────────────────────────────────────
def test_update_branch_stale_reset(
    tmp_path: Path,
    gh_calls: list[dict[str, Any]],
    audit_calls: list[dict[str, Any]],
    decision_calls: list[dict[str, Any]],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """pr=100, head=aaa 1회 trigger → head=bbb 로 변경 → 새 trigger OK.

    회장 §명시 fixture 4 + 12 필수 구현 조건 #8: update-branch 후 기존 trigger
    stale, 새 head 에 대해서만 새 trigger 가능.
    """
    monkeypatch.setenv(OWNER_PAT_ENV_NAME, _FAKE_OWNER_PAT)
    audit_path = tmp_path / "audit.jsonl"
    decision_path = tmp_path / "decision.json"

    trigger = _make_trigger(gh_calls, audit_calls, decision_calls, audit_path)

    # 1회차 — head=aaa
    out1 = trigger.trigger_gemini_review(
        pr_number=100,
        head_sha="aaa",
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=audit_path,
        decision_json_path=decision_path,
    )
    assert out1.outcome == OUTCOME_OK
    assert len(gh_calls) == 1
    assert gh_calls[0]["args"][3] == "/repos/jeon-jonghyuk/taskctl-anu/issues/100/comments"

    # head 변경 (update-branch 시뮬레이션) → head=bbb 로 새 trigger 가능해야 함
    out2 = trigger.trigger_gemini_review(
        pr_number=100,
        head_sha="bbb",
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=audit_path,
        decision_json_path=decision_path,
    )
    assert out2.outcome == OUTCOME_OK
    assert len(gh_calls) == 2

    # audit 에 두 dedupe_key 가 모두 기록되고, 둘이 다름 (stale 처리됨)
    audit_lines = audit_path.read_text(encoding="utf-8").strip().split("\n")
    parsed = [json.loads(line) for line in audit_lines if line.strip()]
    dedupe_keys = {r["dedupe_key"] for r in parsed if r.get("outcome") == OUTCOME_OK}
    assert "100#aaa" in dedupe_keys
    assert "100#bbb" in dedupe_keys
    assert len(dedupe_keys) == 2

    # 단, 같은 새 head (bbb) 로 다시 trigger 하면 또 dedupe 차단
    out3 = trigger.trigger_gemini_review(
        pr_number=100,
        head_sha="bbb",
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=audit_path,
        decision_json_path=decision_path,
    )
    assert out3.outcome == OUTCOME_REJECTED
    assert len(gh_calls) == 2  # 추가 호출 0


# ─── extra: Gemini evidence for current head → next trigger blocked ─────────
def test_gemini_evidence_arrives_for_current_head_blocks_next_cycle(
    tmp_path: Path,
    gh_calls: list[dict[str, Any]],
    audit_calls: list[dict[str, Any]],
    decision_calls: list[dict[str, Any]],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """trigger → mock evidence 도착 (commit_id == current_head) → 다음 사이클 trigger 0.

    회장 §명시 12 필수 구현 조건 #10: trigger 성공 후 Gemini evidence commit_id ==
    current_head 검증. evidence 가 도착하면 같은 head 에 대해 추가 trigger 불필요.

    본 테스트는 dedupe + evidence_state 두 경로 모두 차단됨을 확인.
    """
    monkeypatch.setenv(OWNER_PAT_ENV_NAME, _FAKE_OWNER_PAT)
    audit_path = tmp_path / "audit.jsonl"
    decision_path = tmp_path / "decision.json"

    trigger = _make_trigger(gh_calls, audit_calls, decision_calls, audit_path)

    head = "ev_head_123"
    # 1회 trigger — evidence missing 상태
    out1 = trigger.trigger_gemini_review(
        pr_number=300,
        head_sha=head,
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=audit_path,
        decision_json_path=decision_path,
    )
    assert out1.outcome == OUTCOME_OK
    assert len(gh_calls) == 1

    # evidence 도착 시뮬레이션 — commit_id == current_head (head)
    mock_evidence_commit_id = head
    assert mock_evidence_commit_id == head  # 동일 head 에 도착

    # 다음 사이클 — evidence_state="present_for_current_head" 로 호출
    out2 = trigger.trigger_gemini_review(
        pr_number=300,
        head_sha=head,
        queue_position=0,
        gemini_evidence_state="present_for_current_head",
        audit_log_path=audit_path,
        decision_json_path=decision_path,
    )
    assert out2.outcome == OUTCOME_REJECTED
    # gh 호출 0 (evidence 있으므로 trigger 불필요)
    assert len(gh_calls) == 1
