"""Тесты для scheduler_main.py — standalone scraper-контейнер (#1182).

(a) модуль импортируется без реального DB/сети.
(b) _should_run() возвращает False при SCHEDULER_ENABLE=false.
(c) _run() отменяется чисто без unhandled exceptions.
(d) #1182 Phase 2: SIGTERM → request_shutdown() (не task.cancel); задача
    кооперативно drain'ится; safety-net wait_for-timeout добивает зависшую.
"""

from __future__ import annotations

import asyncio
import os

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

import pytest

import app.scheduler_main as sm
from app.core import shutdown as sd


@pytest.fixture(autouse=True)
def _reset_shutdown() -> None:
    """shutdown — module-global Event: сбрасываем вокруг каждого теста для изоляции."""
    sd.reset_shutdown()
    yield
    sd.reset_shutdown()


# (a) Импорт модуля
def test_scheduler_main_importable() -> None:
    """scheduler_main импортируется без ошибок."""
    assert sm is not None
    assert hasattr(sm, "_should_run")
    assert hasattr(sm, "_run")


# (b) Kill-switch SCHEDULER_ENABLE=false
def test_should_run_false_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
    """_should_run() == False когда SCHEDULER_ENABLE отключён."""
    monkeypatch.setattr(sm.settings, "scheduler_enable", False)
    assert sm._should_run() is False


def test_should_run_true_when_enabled(monkeypatch: pytest.MonkeyPatch) -> None:
    """_should_run() == True при включённом scheduler."""
    monkeypatch.setattr(sm.settings, "scheduler_enable", True)
    assert sm._should_run() is True


# (c) _run() отменяется чисто
@pytest.mark.asyncio
async def test_run_cancels_cleanly(monkeypatch: pytest.MonkeyPatch) -> None:
    """_run() завершается без unhandled exceptions при отмене задачи."""
    cancelled = asyncio.Event()

    async def _stub_loop() -> None:
        """Короткая заглушка: ждёт отмены, имитируя бесконечный kit scheduler_loop."""
        cancelled.set()
        await asyncio.sleep(999)

    # #2397 Part C: _run() безусловно зовёт _run_kit_scheduler() (legacy fallback удалён) —
    # патчим саму функцию на модуле scheduler_main.
    monkeypatch.setattr(sm, "_run_kit_scheduler", _stub_loop)

    task = asyncio.create_task(sm._run())

    # Ждём пока _stub_loop запустится (флаг)
    await asyncio.wait_for(cancelled.wait(), timeout=2.0)

    task.cancel()

    # Не должно бросить никакого исключения
    try:
        await task
    except asyncio.CancelledError:
        pass  # CancelledError от самого task — допустимо

    # Тест прошёл если не было unhandled exception
    assert task.done()


# (d) #1182 Phase 2 — кооперативный SIGTERM-drain
@pytest.mark.asyncio
async def test_await_scheduler_drains_on_shutdown() -> None:
    """_await_scheduler: кооперативная задача видит флаг и выходит сама — без cancel."""
    drained = False

    async def _coop_loop() -> None:
        nonlocal drained
        while not sd.shutdown_requested():
            await asyncio.sleep(0.01)
        drained = True  # докоммитил unit и вышел

    task = asyncio.create_task(_coop_loop())
    runner = asyncio.create_task(sm._await_scheduler(task))

    await asyncio.sleep(0.03)
    assert not runner.done(), "без shutdown _await_scheduler не завершается"

    sd.request_shutdown()
    await asyncio.wait_for(runner, timeout=2.0)

    assert drained is True
    assert task.done()
    assert not task.cancelled(), "кооперативный выход — НЕ hard-cancel"


@pytest.mark.asyncio
async def test_await_scheduler_timeout_hard_cancels(monkeypatch: pytest.MonkeyPatch) -> None:
    """_await_scheduler: некооперативная задача (игнорит флаг) добивается cancel'ом
    по истечении safety-net grace (_DRAIN_TIMEOUT_S)."""
    monkeypatch.setattr(sm, "_DRAIN_TIMEOUT_S", 0.05)

    async def _stuck_loop() -> None:
        # Игнорирует shutdown_requested() — висит вечно.
        await asyncio.Event().wait()

    task = asyncio.create_task(_stuck_loop())
    sd.request_shutdown()  # drain уже запрошен

    await asyncio.wait_for(sm._await_scheduler(task), timeout=2.0)

    assert task.done()
    assert task.cancelled(), "превысила grace → hard-cancel"


@pytest.mark.asyncio
async def test_run_uses_request_shutdown_not_cancel(monkeypatch: pytest.MonkeyPatch) -> None:
    """_run(): когда выставлен shutdown, scheduler_loop сам drain'ится и _run выходит,
    БЕЗ обращения к task.cancel() (кооперативный путь, не hard-cancel)."""
    started = asyncio.Event()

    async def _coop_loop() -> None:
        started.set()
        while not sd.shutdown_requested():
            await asyncio.sleep(0.01)

    # #2397 Part C: _run() безусловно зовёт _run_kit_scheduler() (legacy fallback удалён).
    monkeypatch.setattr(sm, "_run_kit_scheduler", _coop_loop)

    run_task = asyncio.create_task(sm._run())
    await asyncio.wait_for(started.wait(), timeout=2.0)

    # Имитируем то, что делает SIGTERM-хендлер (на Windows add_signal_handler нет).
    sm.request_shutdown()

    await asyncio.wait_for(run_task, timeout=2.0)
    assert run_task.done()
    assert run_task.exception() is None
