"""Offline smoke for scheduler logic."""

import os

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

from datetime import UTC, datetime

import pytest

from app.services.scheduler import ZOMBIE_THRESHOLD_HOURS, compute_next_run_at


def test_compute_next_run_in_normal_window() -> None:
    now = datetime(2026, 5, 23, 12, 0, tzinfo=UTC)
    next_at = compute_next_run_at(2, 5, now=now)
    assert next_at.date() == datetime(2026, 5, 24).date()
    assert 2 <= next_at.hour <= 4  # window [2, 5) UTC


def test_compute_next_run_cross_midnight() -> None:
    now = datetime(2026, 5, 23, 12, 0, tzinfo=UTC)
    # Window 22..3 = 22:00-23:59 + 00:00-02:59
    next_at = compute_next_run_at(22, 3, now=now)
    # Должен попасть либо в [22,23] либо в [0,3) range
    hour = next_at.hour
    assert hour >= 22 or hour < 3


def test_compute_next_run_window_1_hour() -> None:
    now = datetime(2026, 5, 23, 12, 0, tzinfo=UTC)
    # Минимальное окно [3, 4) → ровно 3-й час
    next_at = compute_next_run_at(3, 4, now=now)
    assert next_at.hour == 3


def test_zombie_threshold_constant() -> None:
    assert ZOMBIE_THRESHOLD_HOURS > 0


def test_compute_next_run_is_future() -> None:
    """next_run_at всегда в будущем (завтра) — не в прошлом."""
    now = datetime(2026, 5, 23, 3, 30, tzinfo=UTC)
    next_at = compute_next_run_at(2, 5, now=now)
    assert next_at > now


def test_compute_next_run_timezone_aware() -> None:
    """Результат всегда timezone-aware (UTC)."""
    now = datetime(2026, 5, 23, 12, 0, tzinfo=UTC)
    next_at = compute_next_run_at(2, 5, now=now)
    assert next_at.tzinfo is not None


# ── #750: атомарный _claim_run (advisory lock — anti-double-sweep) ──────────

from collections import deque  # noqa: E402

from app.services import scheduler as _sched  # noqa: E402


class _Scalar:
    def __init__(self, v: object) -> None:
        self._v = v

    def scalar(self) -> object:
        return self._v


class _Fetchone:
    def __init__(self, row: object) -> None:
        self._row = row

    def fetchone(self) -> object:
        return self._row


class _FakeSchedulerDB:
    """Stateful fake моделирующий advisory-lock claim (#750), без реальной БД.

    - SELECT pg_try_advisory_xact_lock → lock_available.
    - SELECT 1 FROM scrape_runs (has_running_run) → из running_results (если задано),
      иначе по флагу self.running[source].
    - UPDATE scrape_schedules → no-op.
    create_run (монкипатчится) выставляет self.running[source]=True.
    """

    def __init__(self, *, lock_available: bool = True, running_results: list[bool] | None = None):
        self.lock_available = lock_available
        self.running: dict[str, bool] = {}
        self._running_results: deque[bool] = deque(running_results or [])
        self.commits = 0
        self.rollbacks = 0

    def execute(self, stmt: object, params: dict | None = None):
        sql = str(stmt)
        if "pg_try_advisory_xact_lock" in sql:
            return _Scalar(self.lock_available)
        if "SELECT 1 FROM scrape_runs" in sql:
            if self._running_results:
                return _Fetchone(object() if self._running_results.popleft() else None)
            src = (params or {}).get("source")
            return _Fetchone(object() if self.running.get(src) else None)
        return _Fetchone(None)  # UPDATE scrape_schedules etc.

    def commit(self) -> None:
        self.commits += 1

    def rollback(self) -> None:
        self.rollbacks += 1


_SCHED_ROW = {
    "source": "avito_city_sweep",
    "window_start_hour": 2,
    "window_end_hour": 5,
    "default_params": {},
}


def test_claim_run_double_call_creates_one_run(monkeypatch: pytest.MonkeyPatch) -> None:
    """#750 DoD: два последовательных _claim_run одного source (нет running) →
    первый run_id, второй None (create_run вызван РОВНО один раз)."""
    db = _FakeSchedulerDB()
    calls = {"n": 0}

    def fake_create_run(_d, *, source, params):
        calls["n"] += 1
        db.running[source] = True
        return 100 + calls["n"]

    monkeypatch.setattr(_sched.runs_mod, "create_run", fake_create_run)

    rid1 = _sched._claim_run(db, _SCHED_ROW)
    rid2 = _sched._claim_run(db, _SCHED_ROW)

    assert rid1 == 101
    assert rid2 is None
    assert calls["n"] == 1  # второй НЕ создал run


def test_claim_run_skips_when_advisory_lock_unavailable(monkeypatch: pytest.MonkeyPatch) -> None:
    """#750: lock занят конкурентным тиком → return None, create_run НЕ вызван."""
    db = _FakeSchedulerDB(lock_available=False)
    calls = {"n": 0}

    def fake_create_run(_d, *, source, params):
        calls["n"] += 1
        return 999

    monkeypatch.setattr(_sched.runs_mod, "create_run", fake_create_run)

    assert _sched._claim_run(db, _SCHED_ROW) is None
    assert calls["n"] == 0


def test_claim_run_rechecks_running_under_lock(monkeypatch: pytest.MonkeyPatch) -> None:
    """#750: running появился МЕЖДУ pre-check и взятием лока (double-checked) →
    return None + rollback (освобождение лока), create_run НЕ вызван."""
    # pre-check → False (нет running), re-check под локом → True (появился).
    db = _FakeSchedulerDB(lock_available=True, running_results=[False, True])
    calls = {"n": 0}

    def fake_create_run(_d, *, source, params):
        calls["n"] += 1
        return 999

    monkeypatch.setattr(_sched.runs_mod, "create_run", fake_create_run)

    assert _sched._claim_run(db, _SCHED_ROW) is None
    assert calls["n"] == 0
    assert db.rollbacks == 1  # лок освобождён
