"""Scheduler 声明、catalog 与 execution state 测试。"""

import asyncio

from unittest.mock import MagicMock

import pytest

from app.application.scheduling import (
    JobCatalog,
    JobExecutionState,
    JobOverlapPolicy,
    JobRecoveryPolicy,
    JobSpec,
)


def test_job_spec_exports_complete_legacy_state() -> None:
    """每个声明必须显式暴露 owner、overlap、timeout、manual 和 recovery。"""
    state = JobSpec(
        "outbox",
        "恢复副作用",
        MagicMock(),
        "outbox",
        timeout_seconds=30,
        recovery=JobRecoveryPolicy.DURABLE_QUEUE,
    ).to_runtime_state()

    assert state["owner"] == "outbox"
    assert state["overlap"] == JobOverlapPolicy.SKIP.value
    assert state["timeout_seconds"] == 30
    assert state["manual"] is False
    assert state["recovery"] == JobRecoveryPolicy.DURABLE_QUEUE.value


def test_job_catalog_rejects_duplicate_ids() -> None:
    """重复 job ID 在 APScheduler 注册前即失败。"""
    spec = JobSpec("same", "相同", MagicMock(), "test")

    with pytest.raises(ValueError, match="不得重复"):
        JobCatalog([spec, spec])


def test_execution_state_skips_overlap_and_records_terminal_state() -> None:
    """统一 execution state 保持旧的跳过重入并记录失败终态。"""
    state = JobSpec("job", "任务", MagicMock(), "test").to_runtime_state()

    assert JobExecutionState.begin(state, "start") is True
    assert JobExecutionState.begin(state, "second") is False
    JobExecutionState.finish(state, "finish", "failed")

    assert state["running"] is False
    assert state["last_started_at"] == "start"
    assert state["last_finished_at"] == "finish"
    assert state["last_error"] == "failed"


@pytest.mark.asyncio
async def test_execution_state_cancels_coroutine_after_timeout() -> None:
    """声明的 timeout 必须取消协程并向 Facade 抛出超时。"""
    cancelled = asyncio.Event()

    async def wait_forever() -> None:
        """模拟只能由取消信号结束的协程任务。"""
        try:
            await asyncio.Event().wait()
        finally:
            cancelled.set()

    with pytest.raises(asyncio.TimeoutError):
        await JobExecutionState.await_result(wait_forever(), timeout_seconds=0.01)

    assert cancelled.is_set()


def test_manual_job_declares_restart_policy() -> None:
    """只允许人工触发的任务必须显式声明重启后不自动补跑。"""
    state = JobSpec(
        "manual",
        "人工任务",
        MagicMock(),
        "runtime",
        manual=True,
        recovery=JobRecoveryPolicy.MANUAL_ONLY,
    ).to_runtime_state()

    assert state["manual"] is True
    assert state["recovery"] == JobRecoveryPolicy.MANUAL_ONLY.value
