"""task-2556 §9 supporting — polling_policy 코드 게이트 단독 회귀.

회장 §명시 2026-05-12 §9: "long polling 금지 — 5~15min normal wait / 30min first timeout /
1회 recheck / 봇 종료" 의 정책 코드 게이트가 각 단위 함수에서 정상 동작하는지 검증.
"""

from __future__ import annotations

import sys
from pathlib import Path

import pytest


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.polling_policy import (  # noqa: E402
    BotSessionExitRequired,
    FIRST_TIMEOUT_SECONDS,
    LongPollingViolation,
    MAX_RECHECKS,
    MAX_SINGLE_SLEEP_SECONDS,
    NORMAL_WAIT_MAX_SECONDS,
    NORMAL_WAIT_MIN_SECONDS,
    PollingState,
    SchedulerCycleBudget,
    advance_recheck,
    assert_first_timeout_not_exceeded,
    assert_normal_wait,
    assert_sleep_allowed,
    consume_sleep_budget,
    must_exit_now,
)


def test_constants_match_chairman_spec():
    """회장 §9 1:1: MAX 900s (15분), FIRST_TIMEOUT 1800s (30분), MAX_RECHECKS 1."""
    assert MAX_SINGLE_SLEEP_SECONDS == 15 * 60
    assert FIRST_TIMEOUT_SECONDS == 30 * 60
    assert MAX_RECHECKS == 1
    assert NORMAL_WAIT_MIN_SECONDS == 5 * 60
    assert NORMAL_WAIT_MAX_SECONDS == 15 * 60


def test_assert_sleep_allowed_rejects_over_15min():
    with pytest.raises(LongPollingViolation):
        assert_sleep_allowed(MAX_SINGLE_SLEEP_SECONDS + 1)


def test_assert_sleep_allowed_rejects_negative_and_non_int():
    with pytest.raises(LongPollingViolation):
        assert_sleep_allowed(-1)
    with pytest.raises(LongPollingViolation):
        assert_sleep_allowed(1.5)  # type: ignore[arg-type]
    with pytest.raises(LongPollingViolation):
        assert_sleep_allowed(True)  # type: ignore[arg-type]


def test_assert_sleep_allowed_accepts_15min_exact():
    assert_sleep_allowed(MAX_SINGLE_SLEEP_SECONDS)  # no raise


def test_assert_normal_wait_lower_bound():
    with pytest.raises(LongPollingViolation):
        assert_normal_wait(NORMAL_WAIT_MIN_SECONDS - 1)


def test_assert_normal_wait_upper_bound():
    with pytest.raises(LongPollingViolation):
        assert_normal_wait(NORMAL_WAIT_MAX_SECONDS + 1)


def test_assert_normal_wait_in_range_ok():
    assert_normal_wait(NORMAL_WAIT_MIN_SECONDS)
    assert_normal_wait(NORMAL_WAIT_MAX_SECONDS)
    assert_normal_wait((NORMAL_WAIT_MIN_SECONDS + NORMAL_WAIT_MAX_SECONDS) // 2)


def test_first_timeout_30min_boundary():
    assert_first_timeout_not_exceeded(0)
    assert_first_timeout_not_exceeded(FIRST_TIMEOUT_SECONDS)
    with pytest.raises(LongPollingViolation):
        assert_first_timeout_not_exceeded(FIRST_TIMEOUT_SECONDS + 1)


def test_advance_recheck_increments_until_max():
    s0 = PollingState(rechecks_done=0)
    s1 = advance_recheck(s0)
    assert s1.rechecks_done == 1
    with pytest.raises(BotSessionExitRequired):
        advance_recheck(s1)


def test_must_exit_now_for_elapsed_over_first_timeout():
    s = PollingState(rechecks_done=0, elapsed_seconds=FIRST_TIMEOUT_SECONDS + 1)
    assert must_exit_now(s) is True


def test_must_exit_now_for_normal_state_false():
    s = PollingState(rechecks_done=0, elapsed_seconds=600)
    assert must_exit_now(s) is False


def test_must_exit_now_for_non_polling_state_returns_true():
    """비 PollingState 입력 시 안전망: exit 강제."""
    assert must_exit_now("not a state") is True  # type: ignore[arg-type]
    assert must_exit_now(None) is True  # type: ignore[arg-type]


def test_consume_sleep_budget_cumulative_violation():
    b = SchedulerCycleBudget()
    b = consume_sleep_budget(b, 600)  # 10 min
    b = consume_sleep_budget(b, 300)  # 15 min total
    with pytest.raises(LongPollingViolation):
        consume_sleep_budget(b, 1)  # 15:01 total - violation


def test_advance_recheck_rejects_non_state():
    with pytest.raises(LongPollingViolation):
        advance_recheck("not a state")  # type: ignore[arg-type]
