"""task-2556 §3 — 30min timeout 후 FIRST_GEMINI_TRIGGER_MISSING 자동 trigger 회귀.

회장 §명시 2026-05-12 §3:
  PR createdAt + 30min 경과 + Gemini reviews 0건 = FIRST_GEMINI_TRIGGER_MISSING.

검증 포인트:
  1. 정확 grace period (30분) 경계 검증 (within / boundary / past).
  2. boundary 직전 (29:59) → WITHIN_GRACE_PERIOD, scheduler 는 SKIP.
  3. 30:00 정각 + reviews 0 → FIRST_GEMINI_TRIGGER_MISSING.
  4. 1h 경과 + reviews 0 → FIRST_GEMINI_TRIGGER_MISSING + scheduler OWNER_TRIGGER_DISPATCHED.
  5. 30:01 경과 + 1 review 도착 (head 가 review commit_id 와 일치) → GEMINI_FRESH_ON_HEAD → SKIP.
"""

from __future__ import annotations

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_FIRST_TRIGGER_PENDING_SKIP,
    ACTION_OWNER_TRIGGER_DISPATCHED,
    ACTION_WITHIN_GRACE,
    ACTION_FRESH_RESUME,
    ExecutorScheduler,
)
from anu_v2.idle_pr_diagnoser import (  # noqa: E402
    FIRST_TRIGGER_PENDING_WINDOW_SECONDS,
    GeminiReviewMeta,
    GRACE_PERIOD_SECONDS,
    IdlePRDiagnoser,
    IdlePRSnapshot,
    STATE_FIRST_GEMINI_TRIGGER_MISSING,
    STATE_FIRST_TRIGGER_PENDING,
    STATE_GEMINI_FRESH_ON_HEAD,
    STATE_WITHIN_GRACE_PERIOD,
)
from anu_v2.polling_policy import FIRST_TIMEOUT_SECONDS  # noqa: E402
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


_HEAD = "a" * 40


def _snapshot(
    *,
    number: int = 200,
    head_sha: str = _HEAD,
    created_at: str = "2026-05-12T10:00:00+00:00",
    reviews: tuple[GeminiReviewMeta, ...] = (),
    ci_pass: bool = True,
) -> IdlePRSnapshot:
    return IdlePRSnapshot(
        number=number,
        head_sha=head_sha,
        head_ref="task/task-2556-dev5",
        created_at=created_at,
        gemini_reviews=reviews,
        ci_required_all_success=ci_pass,
    )


def _make_runner(tmp_path: Path, *, http_calls: list) -> OwnerTriggerOnly:
    def http_post(method, path, body, headers):
        http_calls.append({"method": method, "path": path, "body": body})
        return {"id": 1}

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


def _make_scheduler(
    tmp_path: Path,
    snapshots: list[IdlePRSnapshot],
    *,
    http_calls: list | None = None,
) -> ExecutorScheduler:
    if http_calls is None:
        http_calls = []
    return ExecutorScheduler(
        workspace_root=tmp_path,
        decision_dir=tmp_path / "memory" / "events",
        snapshot_provider=lambda: snapshots,
        owner_trigger=_make_runner(tmp_path, http_calls=http_calls),
        merge_executor=MergeQueueExecutor(
            gh_runner=lambda a, e: {},
            git_runner=lambda a: "",
            pytest_runner=lambda p: 0,
            audit_writer=lambda p: None,
            task_md_root=tmp_path,
        ),
        owner="o",
        repo="r",
    )


# ─── grace period boundary tests ──────────────────────────────────────────


def test_within_grace_29_59_classified_first_trigger_pending():
    """task-2563 §1: 29:59 (1799s) 은 pending_window (300s) 경과 + first_timeout (1800s) 미경과
    + reviews 0 → FIRST_TRIGGER_PENDING (이전엔 WITHIN_GRACE_PERIOD 였음, 회장 §명시 1:1 분리)."""
    diag = IdlePRDiagnoser().diagnose(
        _snapshot(created_at="2026-05-12T10:00:00+00:00"),
        now="2026-05-12T10:29:59+00:00",
    )
    assert diag.state == STATE_FIRST_TRIGGER_PENDING
    assert diag.elapsed_since_created_seconds == 29 * 60 + 59
    # PENDING 은 default 로 owner trigger invoking states 에 포함되지 않음 (fast_path=true 시만 dispatch).
    assert diag.requires_owner_trigger is False


def test_boundary_30_00_exact_classified_first_missing():
    """30:00 정각, reviews 0 → FIRST_GEMINI_TRIGGER_MISSING."""
    diag = IdlePRDiagnoser().diagnose(
        _snapshot(created_at="2026-05-12T10:00:00+00:00"),
        now="2026-05-12T10:30:00+00:00",
    )
    assert diag.state == STATE_FIRST_GEMINI_TRIGGER_MISSING
    assert diag.elapsed_since_created_seconds == FIRST_TIMEOUT_SECONDS
    assert diag.requires_owner_trigger is True
    assert diag.latest_gemini_commit_id is None


def test_past_1h_no_reviews_dispatches_owner_trigger(tmp_path):
    http_calls: list = []
    snap = _snapshot(created_at="2026-05-12T10:00:00+00:00", reviews=())
    scheduler = _make_scheduler(tmp_path, [snap], http_calls=http_calls)
    result = scheduler.run_one_cycle(
        env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxx"},
        now="2026-05-12T11:00:00+00:00",
    )
    assert len(result.pr_actions) == 1
    assert result.pr_actions[0].state == STATE_FIRST_GEMINI_TRIGGER_MISSING
    assert result.pr_actions[0].action == ACTION_OWNER_TRIGGER_DISPATCHED
    assert len(http_calls) == 1
    assert http_calls[0]["body"] == {"body": "/gemini review"}


def test_within_grace_scheduler_skips_no_http_call(tmp_path):
    """task-2563 §1: 15:00 (900s) 은 pending_window 경과 → FIRST_TRIGGER_PENDING_SKIP.
    fast_path=false (default) 이므로 HTTP POST 호출 0."""
    http_calls: list = []
    snap = _snapshot(created_at="2026-05-12T10:00:00+00:00")
    scheduler = _make_scheduler(tmp_path, [snap], http_calls=http_calls)
    result = scheduler.run_one_cycle(
        env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxx"},
        now="2026-05-12T10:15:00+00:00",
    )
    # 900s elapsed = PENDING 구간. fast_path=false default → SKIP, HTTP POST 0.
    assert result.pr_actions[0].action == ACTION_FIRST_TRIGGER_PENDING_SKIP
    assert len(http_calls) == 0


def test_within_grace_short_window_scheduler_skips(tmp_path):
    """task-2563 §1: 200s (짧은 grace window 이내) → WITHIN_GRACE_PERIOD, HTTP POST 0."""
    http_calls: list = []
    snap = _snapshot(created_at="2026-05-12T10:00:00+00:00")
    scheduler = _make_scheduler(tmp_path, [snap], http_calls=http_calls)
    result = scheduler.run_one_cycle(
        env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxx"},
        now="2026-05-12T10:03:20+00:00",  # 200s
    )
    assert result.pr_actions[0].action == ACTION_WITHIN_GRACE
    assert len(http_calls) == 0


def test_fresh_review_after_grace_period_classifies_fresh(tmp_path):
    """30min 경과 + Gemini review (commit_id == head_sha) → GEMINI_FRESH_ON_HEAD."""
    http_calls: list = []
    review = GeminiReviewMeta(commit_id=_HEAD, submitted_at="2026-05-12T10:35:00+00:00")
    snap = _snapshot(created_at="2026-05-12T10:00:00+00:00", reviews=(review,))
    scheduler = _make_scheduler(tmp_path, [snap], http_calls=http_calls)
    result = scheduler.run_one_cycle(
        env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxx"},
        now="2026-05-12T10:36:00+00:00",
    )
    assert result.pr_actions[0].state == STATE_GEMINI_FRESH_ON_HEAD
    assert result.pr_actions[0].action == ACTION_FRESH_RESUME
    assert len(http_calls) == 0


def test_grace_period_constant_matches_first_timeout():
    """task-2563 §1 1:1: GRACE_PERIOD_SECONDS 는 polling_policy.FIRST_TIMEOUT_SECONDS 와 일치 (1800s).
    회장 §명시 "FIRST_TIMEOUT_SECONDS=1800 1:1" 박제."""
    assert GRACE_PERIOD_SECONDS == FIRST_TIMEOUT_SECONDS
    assert GRACE_PERIOD_SECONDS == 30 * 60  # 30 분 = 1800 초


def test_first_trigger_pending_window_default_is_5_minutes():
    """task-2563 §1: "PR open 직후 짧은 시간" = 5 분 default."""
    assert FIRST_TRIGGER_PENDING_WINDOW_SECONDS == 5 * 60
