"""task-2556 §1/§5/§6/§7 — PR #107 실증 fixture 회귀.

회장 §명시 2026-05-12 본질 1:1:
  PR #107 이 실증한 갭 = idle PR 을 자동 감지해 runner 를 호출하는 daemon/scheduler entry
  point 부재. 본 test 는 ``executor_scheduler.ExecutorScheduler.run_one_cycle`` 이 PR #107
  과 같은 상황 (createdAt + 30min, Gemini reviews 0) 에서 자동으로 owner trigger 를
  발사하는지 회귀 검증.

검증 포인트:
  1. snapshot_provider 호출 1 회.
  2. diagnoser 가 FIRST_GEMINI_TRIGGER_MISSING 으로 분류.
  3. emit_owner_trigger_decision → decision.json 생성.
  4. invoke_from_scheduler → owner_trigger_only.trigger_gemini_review 호출.
  5. POSTED marker 생성.
  6. audit JSONL 에 OWNER_TRIGGER_DISPATCHED 박제.
  7. chat_notifications == 0.
  8. bot_should_exit == True (default polling state — recheck 0 이지만 must_exit_now 는
     False, 즉 첫 cycle 종료 후 외부 cron 재진입).
"""

from __future__ import annotations

import json
import sys
from pathlib import 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.executor_scheduler import (  # noqa: E402
    ACTION_OWNER_TRIGGER_DISPATCHED,
    ExecutorScheduler,
    SCHEDULER_AUDIT_REL_PATH,
    SchedulerCycleResult,
)
from anu_v2.idle_pr_diagnoser import (  # noqa: E402
    GeminiReviewMeta,
    IdlePRSnapshot,
    STATE_FIRST_GEMINI_TRIGGER_MISSING,
)
from anu_v2.merge_queue_executor import MergeQueueExecutor  # noqa: E402
from anu_v2.owner_trigger_audit import OwnerTriggerAudit  # noqa: E402
from anu_v2.owner_trigger_only import OwnerTriggerOnly  # noqa: E402


FIXTURE_PATH = Path(__file__).resolve().parents[1] / "fixtures" / "pr107_first_gemini_missing.json"


def _load_fixture() -> dict:
    return json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))


def _build_snapshot(snapshot_dict: dict) -> IdlePRSnapshot:
    reviews = tuple(
        GeminiReviewMeta(commit_id=r["commit_id"], submitted_at=r["submitted_at"])
        for r in snapshot_dict.get("gemini_reviews", [])
    )
    return IdlePRSnapshot(
        number=snapshot_dict["number"],
        head_sha=snapshot_dict["head_sha"],
        head_ref=snapshot_dict["head_ref"],
        created_at=snapshot_dict["created_at"],
        gemini_reviews=reviews,
        ci_required_all_success=snapshot_dict["ci_required_all_success"],
        state=snapshot_dict.get("state", "OPEN"),
        author_is_bot=snapshot_dict.get("author_is_bot", True),
    )


def _build_merge_executor(tmp_path: Path) -> MergeQueueExecutor:
    return MergeQueueExecutor(
        gh_runner=lambda args, env: {},
        git_runner=lambda args: "",
        pytest_runner=lambda paths: 0,
        audit_writer=lambda payload: None,
        task_md_root=tmp_path,
    )


def _build_owner_trigger(
    tmp_path: Path,
    *,
    http_calls: list,
    token: str = "ghp_owner_test_token_xxxxxxxxxxxxxxxx",
) -> OwnerTriggerOnly:
    def http_post(method, path, body, headers):
        http_calls.append({"method": method, "path": path, "body": body})
        return {"id": 1, "html_url": "https://example/comments/1"}

    audit = OwnerTriggerAudit(tmp_path)
    return OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=http_post,
        token_provider=lambda: token,
        audit=audit,
    )


def test_pr107_pilot_first_gemini_missing_dispatches_owner_trigger(tmp_path):
    fixture = _load_fixture()
    snapshot = _build_snapshot(fixture["snapshot"])
    http_calls: list = []
    merge_executor = _build_merge_executor(tmp_path)
    owner_trigger = _build_owner_trigger(tmp_path, http_calls=http_calls)

    decision_dir = tmp_path / "memory" / "events"

    scheduler = ExecutorScheduler(
        workspace_root=tmp_path,
        decision_dir=decision_dir,
        snapshot_provider=lambda: [snapshot],
        owner_trigger=owner_trigger,
        merge_executor=merge_executor,
        owner="test-owner",
        repo="test-repo",
    )

    result = scheduler.run_one_cycle(
        env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_owner_test_token_xxxxxxxxxxxxxxxx"},
        now=fixture["now"],
    )

    assert isinstance(result, SchedulerCycleResult)
    assert len(result.pr_actions) == 1
    action = result.pr_actions[0]
    assert action.pr_number == 107
    assert action.task_id == fixture["expected_diagnosis"]["task_id"]
    assert action.state == STATE_FIRST_GEMINI_TRIGGER_MISSING
    assert action.action == ACTION_OWNER_TRIGGER_DISPATCHED
    assert action.runner_result == "POSTED"

    # chat notification 0
    assert result.chat_notifications == 0

    # HTTP call exactly once, on the allowed endpoint
    assert len(http_calls) == 1
    assert http_calls[0]["method"] == "POST"
    assert http_calls[0]["path"] == "/repos/test-owner/test-repo/issues/107/comments"
    assert http_calls[0]["body"] == {"body": "/gemini review"}

    # marker files exist
    decision_path = decision_dir / "task-2556.owner_trigger_decision.json"
    requested_marker = decision_dir / "task-2556.owner-trigger.requested"
    posted_marker = decision_dir / "task-2556.owner-trigger.posted"
    assert decision_path.exists()
    assert requested_marker.exists()
    assert posted_marker.exists()

    # decision.json schema PASS
    decision = json.loads(decision_path.read_text(encoding="utf-8"))
    assert decision["schema"] == "anu_v2.owner_trigger_decision.v1"
    assert decision["allowed"] is True
    assert decision["gemini_evidence_fresh"] is False
    assert decision["nudge_count_for_pr_head"] == 0
    assert decision["pr"] == 107
    assert decision["current_head"] == fixture["snapshot"]["head_sha"].lower()

    # scheduler audit JSONL append
    audit_path = tmp_path / SCHEDULER_AUDIT_REL_PATH
    assert audit_path.exists()
    audit_lines = [ln for ln in audit_path.read_text(encoding="utf-8").splitlines() if ln]
    assert len(audit_lines) == 1
    audit_rec = json.loads(audit_lines[0])
    assert audit_rec["pr_number"] == 107
    assert audit_rec["action"] == ACTION_OWNER_TRIGGER_DISPATCHED
    assert audit_rec["chat_notifications"] == 0
    assert audit_rec["schema"] == "anu_v2.executor_scheduler.v1"


def test_pr107_pilot_diagnosis_only_when_owner_trigger_dispatched():
    """PR #107 fixture 의 expected diagnosis 가 IdlePRDiagnoser 와 1:1 일치."""
    from anu_v2.idle_pr_diagnoser import IdlePRDiagnoser
    fixture = _load_fixture()
    snapshot = _build_snapshot(fixture["snapshot"])
    diagnoser = IdlePRDiagnoser()
    diag = diagnoser.diagnose(snapshot, now=fixture["now"])
    assert diag.state == fixture["expected_diagnosis"]["state"]
    assert diag.task_id == fixture["expected_diagnosis"]["task_id"]
    assert diag.elapsed_since_created_seconds == fixture["expected_diagnosis"]["elapsed_since_created_seconds"]
    assert diag.requires_owner_trigger is True


def test_pr107_pilot_chat_notifications_always_zero(tmp_path):
    """회장 §8 / 금지 15 — scheduler chat 노출 0."""
    fixture = _load_fixture()
    snapshot = _build_snapshot(fixture["snapshot"])
    merge_executor = _build_merge_executor(tmp_path)
    owner_trigger = _build_owner_trigger(tmp_path, http_calls=[])
    scheduler = ExecutorScheduler(
        workspace_root=tmp_path,
        decision_dir=tmp_path / "events",
        snapshot_provider=lambda: [snapshot],
        owner_trigger=owner_trigger,
        merge_executor=merge_executor,
        owner="test-owner",
        repo="test-repo",
    )
    result = scheduler.run_one_cycle(
        env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_test_xxxxxxxxxxxxxxxxxxxx"},
        now=fixture["now"],
    )
    assert result.chat_notifications == 0
    # audit also enforces 0
    audit_path = tmp_path / SCHEDULER_AUDIT_REL_PATH
    for line in audit_path.read_text(encoding="utf-8").splitlines():
        if not line:
            continue
        rec = json.loads(line)
        assert rec["chat_notifications"] == 0
