"""Unit tests for typed scheduled actions and scheduler persistence."""

from __future__ import annotations

import threading
import time
from types import SimpleNamespace
from typing import cast
from unittest.mock import patch

import pytest
from cryptography.fernet import Fernet
from fastapi.testclient import TestClient

from media_library_viewer_api.dependencies import get_settings_store
from media_library_viewer_api.main import app
from media_library_viewer_api.services.qbittorrent_store import (
    QBITTORRENT_CONCERN,
    QbittorrentSampleStore,
)
from media_library_viewer_api.services.scheduler import (  # type: ignore[reportMissingImports]
    Scheduler,
    SchedulerBusyError,
)
from media_library_viewer_api.services.scheduler_actions import (  # type: ignore[reportMissingImports]
    QBITTORRENT_SPEED_ACTION,
    ActionResult,
)
from media_library_viewer_api.services.scheduler_store import (  # type: ignore[reportMissingImports]
    SCHEDULER_CONCERN,
    SchedulerRunStore,
)
from media_library_viewer_api.services.secrets import reset_encryption_key_cache
from media_library_viewer_api.services.service_data import ServiceDataHarness
from media_library_viewer_api.services.settings_store import SettingsStore
from media_library_viewer_api.widgets.sources import ServiceRecord


@pytest.fixture
def scheduler_client(tmp_path, monkeypatch):
    monkeypatch.setenv("MANAGE_ENCRYPTION_KEY", Fernet.generate_key().decode())
    reset_encryption_key_cache()
    store = SettingsStore(tmp_path / "settings.sqlite")
    store.ensure_defaults()
    app.dependency_overrides[get_settings_store] = lambda: store
    with patch("media_library_viewer_api.auth.get_settings", return_value=SimpleNamespace(auth_enabled=False)):
        yield TestClient(app), store
    app.dependency_overrides.clear()
    reset_encryption_key_cache()


def _service(service_id: str = "svc-1") -> ServiceRecord:
    return ServiceRecord(
        id=service_id,
        service_type="qbittorrent",
        name="qbit",
        config={
            "poll_interval_seconds": 15,
            "sample_retention_seconds": 1_800,
            "sample_max_rows": 1200,
        },
        secrets={},
    )


def test_scheduler_routes_expose_status_history_and_disabled_manual_run(scheduler_client):
    client, store = scheduler_client
    service = store.upsert_service(
        {
            "service_type": "qbittorrent",
            "name": "qbit",
            "config": {"base_url": "http://qbit:8080", "polling_enabled": False},
            "secrets": {},
            "enabled": True,
        }
    )

    status = client.get(f"/api/scheduler/services/{service['id']}/status")
    assert status.status_code == 200
    assert not status.json()["enabled"]

    runs = client.get(f"/api/scheduler/services/{service['id']}/runs")
    assert runs.status_code == 200
    assert runs.json()["items"] == []

    manual = client.post(f"/api/scheduler/services/{service['id']}/run")
    assert manual.status_code == 400


def test_sample_store_applies_time_and_row_limits(tmp_path):
    harness = ServiceDataHarness(tmp_path)
    harness.register(QBITTORRENT_CONCERN)
    harness.run_migrations()
    store = QbittorrentSampleStore(harness)

    now = round(time.time())
    for index in range(70):
        store.append(
            "svc-1",
            now - 20 + index,
            index,
            index,
            retention_seconds=60,
            max_rows=60,
        )

    samples = store.window("svc-1")
    assert len(samples) == 60
    assert samples[0]["ts"] == now - 10


def test_scheduler_records_success_and_manual_run_resets_backoff(tmp_path, monkeypatch):
    harness = ServiceDataHarness(tmp_path)
    harness.register(SCHEDULER_CONCERN)
    harness.run_migrations()
    scheduler = Scheduler()
    scheduler._run_store = SchedulerRunStore(harness)
    service = _service()

    class Action:
        def run(self, value):
            assert value.id == "svc-1"
            return ActionResult(data={"ok": True})

    with patch("media_library_viewer_api.services.scheduler.get_scheduled_action", return_value=Action()):
        result = scheduler._execute(service, "manual")

    assert result["status"] == "success"
    fake_store = cast(
        SettingsStore,
        SimpleNamespace(get_service=lambda service_id: {"id": service_id, "service_type": "qbittorrent", "config": {}}),
    )
    status = scheduler.status("svc-1", store=fake_store)
    assert status["consecutive_failures"] == 0
    assert status["backoff_until"] is None
    runs, total = scheduler._run_store.list_runs("svc-1", QBITTORRENT_SPEED_ACTION)
    assert total == 1
    assert runs[0]["trigger"] == "manual"


def test_scheduler_rejects_overlapping_manual_runs(tmp_path):
    harness = ServiceDataHarness(tmp_path)
    harness.register(SCHEDULER_CONCERN)
    harness.run_migrations()
    scheduler = Scheduler()
    scheduler._run_store = SchedulerRunStore(harness)
    service = _service()
    started = threading.Event()
    release = threading.Event()

    class SlowAction:
        def run(self, value):
            started.set()
            release.wait(2)
            return ActionResult(data={})

    with patch("media_library_viewer_api.services.scheduler.get_scheduled_action", return_value=SlowAction()):
        worker = threading.Thread(target=scheduler._execute, args=(service, "manual"))
        worker.start()
        assert started.wait(1)
        try:
            try:
                scheduler._execute(service, "manual")
            except SchedulerBusyError:
                pass
            else:
                raise AssertionError("expected overlapping run to be rejected")
        finally:
            release.set()
            worker.join(timeout=2)


def test_scheduler_status_marks_never_run_service_stale():
    scheduler = Scheduler()
    store = cast(
        SettingsStore,
        SimpleNamespace(
            get_service=lambda service_id: {
                "id": service_id,
                "service_type": "qbittorrent",
                "enabled": True,
                "config": {},
            }
        ),
    )
    status = scheduler.status("svc-1", store=store)
    assert bool(status["is_stale"])
    assert status["poll_interval_seconds"] == 15
