"""task-2556 §11 — OWNER token 부재 시 TokenBoundaryViolation 회귀.

회장 §명시 2026-05-12 §11:
  "token boundary 검증 — BOT_GITHUB_TOKEN 으로 /gemini review 호출 차단 +
   OWNER token actor 검증"

검증 포인트:
  1. env 가 None 이면 TokenBoundaryViolation.
  2. env 에 OWNER_GEMINI_TRIGGER_TOKEN 누락이면 TokenBoundaryViolation.
  3. env 에 BOT_GITHUB_TOKEN 이 있으면 TokenBoundaryViolation.
  4. env 에 GH_TOKEN/GITHUB_TOKEN/OWNER_PAT/PAT_TOKEN 어느 것이라도 있으면 violation.
  5. 빈 문자열 token 도 violation.
  6. fixture (executor_token_unavailable.json) 결과 어셀션.
  7. token 부재 시 http_post 호출 0, decision.json 미생성.
  8. 또한 polling_policy long polling 게이트 통합 — bot session 종료 후 재진입 확인.
"""

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.executor_scheduler import ExecutorScheduler  # noqa: E402
from anu_v2.idle_pr_diagnoser import IdlePRSnapshot  # 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 (  # noqa: E402
    OwnerTriggerOnly,
    TokenBoundaryViolation,
    assert_scheduler_token_boundary,
)
from anu_v2.polling_policy import (  # noqa: E402
    BotSessionExitRequired,
    PollingState,
    MAX_RECHECKS,
    advance_recheck,
    must_exit_now,
)


def _snap():
    return IdlePRSnapshot(
        number=200,
        head_sha="a" * 40,
        head_ref="task/task-2556-dev5",
        created_at="2026-05-12T10:00:00+00:00",
        gemini_reviews=(),
        ci_required_all_success=True,
    )


def _make_scheduler(tmp_path: Path) -> tuple[ExecutorScheduler, list]:
    http_calls: list = []

    def http_post(method, path, body, headers):
        http_calls.append({"path": path, "body": body})
        return {"id": 1}

    runner = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=http_post,
        token_provider=lambda: "ghp_x_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        audit=OwnerTriggerAudit(tmp_path),
    )
    scheduler = ExecutorScheduler(
        workspace_root=tmp_path,
        decision_dir=tmp_path / "memory" / "events",
        snapshot_provider=lambda: [_snap()],
        owner_trigger=runner,
        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",
    )
    return scheduler, http_calls


def test_env_none_raises_token_boundary_violation():
    with pytest.raises(TokenBoundaryViolation):
        assert_scheduler_token_boundary(None)


def test_env_missing_owner_token_raises_violation():
    with pytest.raises(TokenBoundaryViolation) as exc:
        assert_scheduler_token_boundary({})
    assert "OWNER_GEMINI_TRIGGER_TOKEN" in str(exc.value)


def test_env_bot_token_present_raises_violation():
    with pytest.raises(TokenBoundaryViolation) as exc:
        assert_scheduler_token_boundary({
            "BOT_GITHUB_TOKEN": "ghp_bot_xxxx",
            "OWNER_GEMINI_TRIGGER_TOKEN": "ghp_owner_xxxx",
        })
    assert "BOT_GITHUB_TOKEN" in str(exc.value)


@pytest.mark.parametrize(
    "forbidden_env_name",
    ["BOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN", "OWNER_PAT", "PAT_TOKEN"],
)
def test_all_forbidden_token_names_blocked(forbidden_env_name):
    with pytest.raises(TokenBoundaryViolation) as exc:
        assert_scheduler_token_boundary({
            forbidden_env_name: "x",
            "OWNER_GEMINI_TRIGGER_TOKEN": "ghp_owner_xxxx",
        })
    assert forbidden_env_name in str(exc.value)


def test_empty_string_token_raises_violation():
    with pytest.raises(TokenBoundaryViolation) as exc:
        assert_scheduler_token_boundary({"OWNER_GEMINI_TRIGGER_TOKEN": ""})
    assert "empty" in str(exc.value).lower()


def test_run_one_cycle_blocks_when_env_missing(tmp_path):
    scheduler, http_calls = _make_scheduler(tmp_path)
    with pytest.raises(TokenBoundaryViolation):
        scheduler.run_one_cycle(env={}, now="2026-05-12T11:00:00+00:00")
    # http_post 호출 0
    assert len(http_calls) == 0
    # decision.json 미생성
    decision_path = tmp_path / "memory" / "events" / "task-2556.owner_trigger_decision.json"
    assert not decision_path.exists()


def test_run_one_cycle_blocks_when_bot_token_injected(tmp_path):
    scheduler, http_calls = _make_scheduler(tmp_path)
    with pytest.raises(TokenBoundaryViolation):
        scheduler.run_one_cycle(
            env={
                "BOT_GITHUB_TOKEN": "ghp_bot_xxxx",
                "OWNER_GEMINI_TRIGGER_TOKEN": "ghp_owner_xxxx",
            },
            now="2026-05-12T11:00:00+00:00",
        )
    assert len(http_calls) == 0


def test_token_unavailable_fixture_cases_all_raise():
    import json
    fixture_path = (
        Path(__file__).resolve().parents[1] / "fixtures" / "executor_token_unavailable.json"
    )
    fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
    for case in fixture["env_cases"]:
        env = case["env"]
        with pytest.raises(TokenBoundaryViolation) as exc:
            assert_scheduler_token_boundary(env)
        assert (
            case["expected_message_contains"].lower() in str(exc.value).lower()
        ), f"case={case['label']}: expected {case['expected_message_contains']!r} in {exc.value!r}"


# ─── §9 / §12 long polling 게이트 + bot session 재진입 ────────────────────


def test_bot_session_exit_required_after_max_rechecks():
    """recheck 1 회 후 즉시 봇 종료, 다음 cycle 은 외부 cron 책임."""
    state = PollingState(rechecks_done=0, elapsed_seconds=600)
    state_after_first = advance_recheck(state)
    assert state_after_first.rechecks_done == 1
    # 다음 recheck 시도는 BotSessionExitRequired
    with pytest.raises(BotSessionExitRequired):
        advance_recheck(state_after_first)


def test_must_exit_now_at_max_rechecks_true():
    state = PollingState(rechecks_done=MAX_RECHECKS, elapsed_seconds=0)
    assert must_exit_now(state) is True


def test_run_one_cycle_raises_when_already_at_exit_state(tmp_path):
    scheduler, http_calls = _make_scheduler(tmp_path)
    exit_state = PollingState(rechecks_done=MAX_RECHECKS, elapsed_seconds=0)
    with pytest.raises(BotSessionExitRequired):
        scheduler.run_one_cycle(
            env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_x_xxxxxxxxxxxxxxxxxxxx"},
            now="2026-05-12T11:00:00+00:00",
            cycle_polling_state=exit_state,
        )
    assert len(http_calls) == 0


def test_scheduler_audit_persists_for_next_cycle_resume(tmp_path):
    """state persisted markers 기반 재진입 확인 (회장 §12).

    1st cycle 종료 후 audit JSONL + decision.json + POSTED marker 가 박제되어
    2nd cycle (외부 cron) 이 동일 (pr, head) 를 보면 SAME_HEAD_DEDUPED 로 fail-closed.
    """
    scheduler, http_calls = _make_scheduler(tmp_path)
    # 1st cycle: dispatches owner trigger
    scheduler.run_one_cycle(
        env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_x_xxxxxxxxxxxxxxxxxxxx"},
        now="2026-05-12T11:00:00+00:00",
    )
    assert len(http_calls) == 1
    # 2nd cycle (외부 cron 시뮬레이션): 동일 snapshot — SAME_HEAD_DEDUPED
    result2 = scheduler.run_one_cycle(
        env={"OWNER_GEMINI_TRIGGER_TOKEN": "ghp_x_xxxxxxxxxxxxxxxxxxxx"},
        now="2026-05-12T11:05:00+00:00",
    )
    assert result2.pr_actions[0].action == "SAME_HEAD_DEDUPED"
    # http_post 누적 1 (재호출 없음)
    assert len(http_calls) == 1
