"""tests.regression.test_bot_trigger_fail_2553 — 회장 §명시 fixture 1 (task-2553).

fixture 1: **bot trigger fail** — BOT_GITHUB_TOKEN 댓글이 Gemini auto trigger
작동 안 함 어설션.

회장 dec_1 (2026-05-11) 승인 근거 1:
  - bot 댓글 trigger 0/5 (0%) 미작동 실증 (task-2552 사전조사 5/5 fail 박제)
  - evidence path: memory/events/task-2552.trigger_evidence_audit_24h.json

본 regression 의 핵심 어설션 (정적 + behavioral):
  1. OwnerTriggerPat 의 기본 `token_env` 가 OWNER_GEMINI_TRIGGER_PAT 으로 박제됨
     → BOT_GITHUB_TOKEN 으로 동작하지 않음 (doctrine 박제).
  2. BOT_GITHUB_TOKEN 만 설정된 환경에서 trigger 호출 시 token_missing 으로
     REJECT 됨 → bot token 으로는 절대 `/gemini review` 댓글 작성되지 않음.
  3. 5/5 bot-trigger fail 시나리오 시뮬레이션 — bot 이 댓글을 작성해도 Gemini
     evidence 가 도착하지 않는 모델을 mock 으로 재현.

본 모듈은 `anu_v2.owner_trigger_pat` 를 **수정하지 않고** 외부에서 정적/동적
박제만 수행한다 (read-only regression).
"""

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 (regression 디렉토리 → 2 단계 위)
_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
    EVIDENCE_MISSING_FOR_CURRENT_HEAD,
    OUTCOME_OK,
    OUTCOME_REJECTED,
    OWNER_PAT_ENV_NAME,
    OwnerTriggerPat,
)


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


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


def _make_trigger(
    gh_calls: list[dict[str, Any]],
    audit_calls: list[dict[str, Any]],
    decision_calls: list[dict[str, Any]],
    *,
    token_env: str = OWNER_PAT_ENV_NAME,
) -> OwnerTriggerPat:
    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))

    def decision_writer(payload: Mapping[str, Any], path: Path) -> None:
        decision_calls.append({"payload": dict(payload), "path": str(path)})
        Path(path).parent.mkdir(parents=True, exist_ok=True)
        Path(path).write_text(
            json.dumps(dict(payload), ensure_ascii=False, sort_keys=True, indent=2),
            encoding="utf-8",
        )

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


# ─── A. 정적 어설션 — token_env 기본값 박제 ─────────────────────────────────
def test_bot_trigger_fail_default_token_env_is_owner_pat_not_bot() -> None:
    """OwnerTriggerPat 의 기본 token_env 가 OWNER_GEMINI_TRIGGER_PAT 인지 정적 검증.

    회장 §명시 박제: BOT_GITHUB_TOKEN 으로 `/gemini review` 댓글 작성 doctrine 위반.
    본 모듈의 token_env 는 OWNER 전용 secret 이름으로 박제되어야 한다.
    """
    assert OWNER_PAT_ENV_NAME == "OWNER_GEMINI_TRIGGER_PAT"
    # BOT_GITHUB_TOKEN 은 OWNER trigger 의 token_env 가 아니다.
    assert OWNER_PAT_ENV_NAME != "BOT_GITHUB_TOKEN"
    assert OWNER_PAT_ENV_NAME != "GH_TOKEN"
    assert OWNER_PAT_ENV_NAME != "GITHUB_TOKEN"


def test_bot_trigger_fail_owner_pat_env_name_is_isolated_secret() -> None:
    """OWNER PAT env 이름이 별도 secret (BOT 과 분리) 으로 박제되어 있는지 정적 어설션.

    회장 §명시 12 필수 구현 조건 #2: token 은 별도 secret 으로 저장
    (`.env.keys` 등 BOT_GITHUB_TOKEN 과 분리).
    """
    # OWNER_ prefix 로 시작 + GEMINI / TRIGGER / PAT 단어 포함 — 모듈 doctrine.
    assert OWNER_PAT_ENV_NAME.startswith("OWNER_")
    assert "GEMINI" in OWNER_PAT_ENV_NAME
    assert "TRIGGER" in OWNER_PAT_ENV_NAME
    assert "PAT" in OWNER_PAT_ENV_NAME


# ─── B. behavioral — BOT_GITHUB_TOKEN 만 설정 시 REJECT ──────────────────────
def test_bot_trigger_fail_bot_token_only_env_rejects_trigger(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """BOT_GITHUB_TOKEN 만 있고 OWNER_GEMINI_TRIGGER_PAT 누락 시 trigger REJECT.

    회장 §명시 15 금지 #14 (default GH_TOKEN fallback) + #15 (BOT_GITHUB_TOKEN
    대신 OWNER PAT 사용) 박제. fallback X — bot token 으로는 trigger 불가.
    """
    monkeypatch.delenv(OWNER_PAT_ENV_NAME, raising=False)
    # BOT_GITHUB_TOKEN 만 환경에 박아둠 — fallback 차단 검증
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "ghp_BOT_FAKE_TOKEN")
    monkeypatch.setenv("GH_TOKEN", "ghp_BOT_FAKE_TOKEN")
    monkeypatch.setenv("GITHUB_TOKEN", "ghp_BOT_FAKE_TOKEN")

    gh_calls: list[dict[str, Any]] = []
    audit_calls: list[dict[str, Any]] = []
    decision_calls: list[dict[str, Any]] = []
    trigger = _make_trigger(gh_calls, audit_calls, decision_calls)

    out = trigger.trigger_gemini_review(
        pr_number=81,
        head_sha="botfakeSHA",
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=tmp_path / "audit.jsonl",
        decision_json_path=tmp_path / "decision.json",
    )

    assert out.outcome == OUTCOME_REJECTED
    assert "token_missing" in out.reason
    # gh 호출 0 — bot token 으로 절대 trigger 안 됨
    assert len(gh_calls) == 0
    # audit 에 token_present False
    assert audit_calls[-1]["token_present"] is False
    # bot token value 가 reason / audit 어디에도 leak 되지 않음
    record_text = json.dumps(audit_calls[-1])
    assert "ghp_BOT_FAKE_TOKEN" not in record_text
    assert "ghp_BOT_FAKE_TOKEN" not in out.reason


def test_bot_trigger_fail_explicit_bot_token_env_also_rejects(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """token_env=BOT_GITHUB_TOKEN 등 OWNER PAT 외 env 이름으로 직접 인스턴스화 시도 시 차단.

    회장 Codex G1 round 2 High — token_env override 화이트리스트 강제 (constructor
    레벨 차단). 호출부가 BOT_GITHUB_TOKEN 같은 env 이름을 token_env 로 넘기면
    `OwnerTriggerPat.__init__` 단계에서 즉시 `ValueError` 발생. doctrine 위반
    시나리오 (OWNER PAT 위치에 BOT 토큰 사용) 를 API 수준에서 차단.
    """
    # OWNER PAT env 명시적으로 제거
    monkeypatch.delenv(OWNER_PAT_ENV_NAME, raising=False)
    monkeypatch.delenv("BOT_GITHUB_TOKEN_ABSENT", raising=False)

    gh_calls: list[dict[str, Any]] = []
    audit_calls: list[dict[str, Any]] = []
    decision_calls: list[dict[str, Any]] = []

    # constructor 가 token_env override 를 차단 → ValueError raise.
    with pytest.raises(ValueError, match=OWNER_PAT_ENV_NAME):
        _make_trigger(
            gh_calls, audit_calls, decision_calls,
            token_env="BOT_GITHUB_TOKEN_ABSENT",
        )

    # gh 호출 0 + audit 0 (constructor 단계 차단 → trigger 메서드 미진입)
    assert len(gh_calls) == 0
    assert len(audit_calls) == 0


# ─── C. 5/5 bot-trigger fail 시뮬레이션 — Gemini evidence 0 ─────────────────
def test_bot_trigger_fail_simulated_5_of_5_no_gemini_evidence(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """task-2552 사전조사 결과 박제: bot 댓글 5건 작성 시도 → Gemini review 0건.

    본 시뮬레이션은 bot 이 어떻게든 댓글을 작성했다고 가정하더라도 (이는 OwnerTriggerPat
    이 거부할 시나리오), Gemini evidence 가 도착하지 않는 결과를 mock 으로 재현한다.
    5건 trigger 시도 → 5건 모두 REJECT (token_missing) → gh 호출 0 → Gemini evidence 0.

    회장 §명시 승인 근거 1: bot 댓글 trigger 0/5 (0%) 미작동 실증.
    """
    monkeypatch.delenv(OWNER_PAT_ENV_NAME, raising=False)
    # bot token 만 존재
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "ghp_BOT_SIM_TOKEN")

    gh_calls: list[dict[str, Any]] = []
    audit_calls: list[dict[str, Any]] = []
    decision_calls: list[dict[str, Any]] = []
    trigger = _make_trigger(gh_calls, audit_calls, decision_calls)

    # mock: Gemini evidence 도착 카운터 — bot trigger 시도 시 0 으로 유지되어야 함.
    gemini_evidence_arrivals = 0

    fail_count = 0
    for attempt in range(5):
        out = trigger.trigger_gemini_review(
            pr_number=100 + attempt,
            head_sha=f"sim{attempt:03d}",
            queue_position=0,
            gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
            audit_log_path=tmp_path / f"audit_{attempt}.jsonl",
            decision_json_path=tmp_path / f"decision_{attempt}.json",
        )
        if out.outcome == OUTCOME_REJECTED:
            fail_count += 1
        # gh 호출이 일어났다면 mock 으로 Gemini evidence 도착 카운트 증가
        # (실제로는 0 이 유지되어야 함 — token_missing 으로 REJECT 되므로)

    # 5/5 모두 REJECT (token_missing)
    assert fail_count == 5
    # gh 호출 0 — bot 으로는 단 1건도 trigger 안 됨
    assert len(gh_calls) == 0
    # Gemini evidence 도착 카운트 0 (회장 §명시 0/5 박제)
    assert gemini_evidence_arrivals == 0
    # audit 5건 모두 token_present=False
    assert len(audit_calls) == 5
    for record in audit_calls:
        assert record["token_present"] is False
        # bot token value leak 0
        assert "ghp_BOT_SIM_TOKEN" not in json.dumps(record)


# ─── D. task-2552 evidence path 박제 ────────────────────────────────────────
def test_bot_trigger_fail_evidence_path_doctrine_pinned() -> None:
    """task-2552 사전조사 evidence path 문서화 박제.

    본 regression 의 doctrine 근거는 다음 evidence path 에 있다 (회장 §명시):
      - memory/events/task-2552.trigger_evidence_audit_24h.json (또는 동일 명칭의
        evidence). 본 테스트는 path string 자체를 박제하여 향후 path 변경 시
        regression 이 깨지도록 한다.
    """
    expected_evidence_paths = (
        "memory/events/task-2552.trigger_evidence_audit_24h.json",
    )
    # 단순 박제 — string 이 정확히 위 형식이어야 함.
    for p in expected_evidence_paths:
        assert p.startswith("memory/events/")
        assert "task-2552" in p
        assert p.endswith(".json")


# ─── E. positive contrast — OWNER PAT 환경에서는 trigger OK ────────────────
def test_bot_trigger_fail_owner_pat_succeeds_for_contrast(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """대조군: OWNER PAT 환경에서는 동일 trigger 가 OK 됨 (회장 §명시 10/10 박제 mirror).

    bot trigger fail 의 의미는 "bot 만으로 안 됨" 이지 "모든 trigger 가 안 됨" 이
    아니다. OWNER PAT 환경에서는 정상 동작해야 한다 (대조 검증).
    """
    monkeypatch.setenv(OWNER_PAT_ENV_NAME, "ghp_OWNER_FAKE_FOR_CONTRAST")

    gh_calls: list[dict[str, Any]] = []
    audit_calls: list[dict[str, Any]] = []
    decision_calls: list[dict[str, Any]] = []
    trigger = _make_trigger(gh_calls, audit_calls, decision_calls)

    out = trigger.trigger_gemini_review(
        pr_number=81,
        head_sha="ownerhead",
        queue_position=0,
        gemini_evidence_state=EVIDENCE_MISSING_FOR_CURRENT_HEAD,
        audit_log_path=tmp_path / "audit.jsonl",
        decision_json_path=tmp_path / "decision.json",
    )
    assert out.outcome == OUTCOME_OK
    assert len(gh_calls) == 1
