"""Тесты beat-таски scrape_freshness_check — алёрт в GlitchTip при stale/failed источниках.

Сеть/БД не дёргаются: мокаем SessionLocal + compute_freshness (единый источник истины) +
sentry_sdk.capture_message. Проверяем: severity-маппинг (error при failed критичного,
warning при stale), сообщение называет проблемные источники, all-ok → нет алёрта, ошибка
агрегации → graceful (исключение не пробрасывается).
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch


def _source(
    name: str,
    status: str,
    *,
    critical: bool = False,
    age_days: float | None = 1.0,
) -> dict[str, Any]:
    """Минимальный source-dict в форме compute_freshness (нужные таске поля)."""
    return {"source": name, "status": status, "critical": critical, "age_days": age_days}


def _payload(sources: list[dict[str, Any]], overall: str) -> dict[str, Any]:
    return {"generated_at": "2026-06-25T09:00:00+00:00", "overall_status": overall,
            "sources": sources}


def _run_with(payload_or_exc: Any) -> tuple[dict[str, Any], MagicMock]:
    """Запускает таску с замоканными SessionLocal/compute_freshness/capture_message.

    payload_or_exc: dict → возвращается из compute_freshness; Exception → бросается.
    Возвращает (summary, capture_message_mock).
    """
    from app.workers.tasks import scrape_freshness_check as task_mod

    db = MagicMock()
    if isinstance(payload_or_exc, Exception):
        compute = MagicMock(side_effect=payload_or_exc)
    else:
        compute = MagicMock(return_value=payload_or_exc)

    with (
        patch.object(task_mod, "SessionLocal", return_value=db),
        patch.object(task_mod, "compute_freshness", compute),
        patch.object(task_mod.sentry_sdk, "capture_message") as cap,
    ):
        summary = task_mod.scrape_freshness_check.run()

    # Сессию всегда закрываем (try/finally).
    db.close.assert_called_once()
    return summary, cap


def test_stale_and_failed_emit_single_alert() -> None:
    """Один stale + один failed (некритичный) → ровно один capture_message, level=warning,
    сообщение называет оба источника."""
    payload = _payload(
        [
            _source("kn", "ok", critical=True),
            _source("objective", "stale", critical=True, age_days=10.0),
            _source("nspd", "failed", critical=False, age_days=None),
        ],
        overall="stale",
    )
    summary, cap = _run_with(payload)

    cap.assert_called_once()
    args, kwargs = cap.call_args
    message = args[0]
    assert kwargs["level"] == "warning"  # упал НЕкритичный → warning
    assert "objective" in message
    assert "nspd" in message
    assert "kn" not in message  # ok-источник не упоминается

    assert summary["alerted"] is True
    assert summary["level"] == "warning"
    assert summary["stale"] == 1
    assert summary["failed"] == 1
    assert summary["alertable"] == 2
    assert summary["checked"] == 3


def test_critical_failed_escalates_to_error() -> None:
    """Критичный источник полностью упал (failed) → level=error."""
    payload = _payload(
        [
            _source("kn", "failed", critical=True, age_days=None),
            _source("nspd", "stale", critical=False, age_days=12.0),
        ],
        overall="failed",
    )
    summary, cap = _run_with(payload)

    cap.assert_called_once()
    _, kwargs = cap.call_args
    assert kwargs["level"] == "error"
    assert summary["level"] == "error"
    assert summary["alerted"] is True


def test_all_ok_emits_no_alert() -> None:
    """Все источники ok → capture_message НЕ вызывается."""
    payload = _payload(
        [
            _source("kn", "ok", critical=True),
            _source("objective", "ok", critical=True),
            _source("nspd", "ok"),
        ],
        overall="ok",
    )
    summary, cap = _run_with(payload)

    cap.assert_not_called()
    assert summary["alerted"] is False
    assert summary["level"] is None
    assert summary["alertable"] == 0
    assert summary["overall_status"] == "ok"


def test_compute_freshness_raising_is_graceful() -> None:
    """compute_freshness бросает → таска логирует и возвращает {'error': ...}, не падает."""
    summary, cap = _run_with(RuntimeError("db boom"))

    cap.assert_not_called()
    assert summary == {"error": "db boom"}


def test_endpoint_delegates_to_compute_freshness() -> None:
    """Рефактор: HTTP-эндпоинт scrape_freshness просто возвращает compute_freshness(db).

    Без живой БД: monkeypatch compute_freshness → проверяем, что handler отдаёт его
    результат байт-в-байт и прокидывает ту же сессию.
    """
    from app.api.v1 import admin_scrape

    sentinel = {"generated_at": "x", "overall_status": "ok", "sources": []}
    db = MagicMock()
    with patch.object(admin_scrape, "compute_freshness", return_value=sentinel) as comp:
        result = admin_scrape.scrape_freshness(db)

    comp.assert_called_once_with(db)
    assert result is sentinel
