from __future__ import annotations

from types import SimpleNamespace
from typing import Callable

import scheduler


def test_start_background_thread_spawns_daemon_thread(monkeypatch) -> None:
    created: list[object] = []

    class _FakeThread:
        def __init__(self, target, daemon: bool = False) -> None:
            self.target = target
            self.daemon = daemon
            self.started = False
            created.append(self)

        def start(self) -> None:
            self.started = True

    monkeypatch.setattr(scheduler.threading, "Thread", _FakeThread)

    marker: list[str] = []
    thread = scheduler.start_background_thread(lambda: marker.append("ran"))

    assert created == [thread]
    assert thread.daemon is True
    assert thread.started is True
    assert marker == []


def test_run_refresh_account_queues_snapshot_on_idle_dispatch() -> None:
    queued: list[Callable[[], None]] = []
    account = SimpleNamespace(slug="rafa", account_home="/tmp/rafa")
    scheduler_instance = scheduler.RefreshScheduler(
        idle_add=lambda callback, *args: queued.append(lambda: callback(*args)) or "idle-token",
        thread_runner=lambda target: target(),
        time_fn=lambda: 1000.0,
    )

    fetch_calls: list[object] = []
    applied: list[tuple[object, object, int | None]] = []

    def fetch_snapshot(account_arg):
        fetch_calls.append(account_arg)
        return {"status": "ok"}

    def apply_snapshot(account_arg, snapshot_arg, generation_arg):
        applied.append((account_arg, snapshot_arg, generation_arg))

    result = scheduler_instance.run_refresh_account(
        account,
        7,
        fetch_snapshot,
        apply_snapshot,
    )

    # fetch_snapshot runs immediately (background thread); apply_snapshot is only
    # scheduled — it must not run until the queued idle callback is actually invoked
    # (mirrors real GLib.idle_add, which defers to the main loop).
    assert fetch_calls == [account]
    assert applied == []
    assert result == "idle-token"

    queued[0]()
    assert applied == [(account, {"status": "ok"}, 7)]


def test_run_refresh_account_routes_fetch_failure_to_on_error() -> None:
    account = SimpleNamespace(slug="rafa", account_home="/tmp/rafa")
    scheduler_instance = scheduler.RefreshScheduler(
        idle_add=lambda callback, *args: callback(*args),
        thread_runner=lambda target: target(),
        time_fn=lambda: 1000.0,
    )
    boom = RuntimeError("boom")

    def fetch_snapshot(account_arg):
        raise boom

    def apply_snapshot(account_arg, snapshot_arg, generation_arg):
        raise AssertionError("apply_snapshot must not run when fetch_snapshot raises")

    errors: list[tuple[object, int | None, Exception]] = []

    def on_error(account_arg, generation_arg, exc):
        errors.append((account_arg, generation_arg, exc))

    scheduler_instance.run_refresh_account(
        account,
        7,
        fetch_snapshot,
        apply_snapshot,
        on_error,
    )

    assert errors == [(account, 7, boom)]


def test_run_refresh_account_routes_apply_snapshot_failure_to_on_error() -> None:
    account = SimpleNamespace(slug="rafa", account_home="/tmp/rafa")
    scheduler_instance = scheduler.RefreshScheduler(
        idle_add=lambda callback, *args: callback(*args),
        thread_runner=lambda target: target(),
        time_fn=lambda: 1000.0,
    )
    boom = RuntimeError("apply failed")

    def fetch_snapshot(account_arg):
        return {"status": "ok"}

    def apply_snapshot(account_arg, snapshot_arg, generation_arg):
        raise boom

    errors: list[tuple[object, int | None, Exception]] = []

    def on_error(account_arg, generation_arg, exc):
        errors.append((account_arg, generation_arg, exc))

    scheduler_instance.run_refresh_account(
        account,
        3,
        fetch_snapshot,
        apply_snapshot,
        on_error,
    )

    assert errors == [(account, 3, boom)]


def test_run_refresh_account_routes_deferred_apply_snapshot_failure_to_on_error() -> None:
    # Real GLib.idle_add does not invoke the callback inline — it only schedules it
    # for the main loop. apply_snapshot's own exception must still reach on_error
    # (and thus still finalize the refresh generation) once that deferred callback
    # actually runs, not just when idle_add is a synchronous stand-in.
    queued: list[Callable[[], None]] = []
    account = SimpleNamespace(slug="rafa", account_home="/tmp/rafa")
    scheduler_instance = scheduler.RefreshScheduler(
        idle_add=lambda callback, *args: queued.append(lambda: callback(*args)) or "idle-token",
        thread_runner=lambda target: target(),
        time_fn=lambda: 1000.0,
    )
    boom = RuntimeError("apply failed later")

    def fetch_snapshot(account_arg):
        return {"status": "ok"}

    def apply_snapshot(account_arg, snapshot_arg, generation_arg):
        raise boom

    errors: list[tuple[object, int | None, Exception]] = []

    def on_error(account_arg, generation_arg, exc):
        errors.append((account_arg, generation_arg, exc))

    scheduler_instance.run_refresh_account(
        account,
        3,
        fetch_snapshot,
        apply_snapshot,
        on_error,
    )

    # Not yet invoked: on_error must not fire until the main loop actually runs
    # the queued idle callback.
    assert errors == []

    queued[0]()

    assert errors == [(account, 3, boom)]


def test_run_refresh_account_without_on_error_swallows_exception() -> None:
    account = SimpleNamespace(slug="rafa", account_home="/tmp/rafa")
    scheduler_instance = scheduler.RefreshScheduler(
        idle_add=lambda callback, *args: callback(*args),
        thread_runner=lambda target: target(),
        time_fn=lambda: 1000.0,
    )

    def fetch_snapshot(account_arg):
        raise RuntimeError("boom")

    def apply_snapshot(account_arg, snapshot_arg, generation_arg):
        raise AssertionError("must not run")

    result = scheduler_instance.run_refresh_account(
        account,
        None,
        fetch_snapshot,
        apply_snapshot,
    )

    assert result is None


def test_refresh_all_tracks_generation_and_pending_completion() -> None:
    accounts = [SimpleNamespace(slug="rafa"), SimpleNamespace(slug="roy")]
    scheduler_instance = scheduler.RefreshScheduler(time_fn=lambda: 1000.0)
    scheduled: list[tuple[str, int | None]] = []
    empty_calls: list[str] = []
    completed: list[str] = []

    generation = scheduler_instance.refresh_all(
        accounts,
        lambda account: None,
        3600,
        True,
        lambda account, generation: scheduled.append((account.slug, generation)),
        lambda: empty_calls.append("empty"),
    )

    assert generation == 1
    assert scheduled == [("rafa", 1), ("roy", 1)]
    assert empty_calls == []
    assert scheduler_instance._pending_refreshes == {1: 2}

    scheduler_instance.finish_refresh(1, lambda: completed.append("done"))
    assert completed == []
    assert scheduler_instance._pending_refreshes == {1: 1}

    scheduler_instance.finish_refresh(1, lambda: completed.append("done"))
    assert completed == ["done"]
    assert scheduler_instance._pending_refreshes == {}


def test_refresh_all_without_due_accounts_runs_empty_callback() -> None:
    accounts = [SimpleNamespace(slug="rafa")]
    scheduler_instance = scheduler.RefreshScheduler(time_fn=lambda: 1000.0)
    scheduled: list[tuple[str, int | None]] = []
    empty_calls: list[str] = []

    generation = scheduler_instance.refresh_all(
        accounts,
        lambda _account: 999.5,
        3600,
        False,
        lambda account, generation: scheduled.append((account.slug, generation)),
        lambda: empty_calls.append("empty"),
    )

    assert generation is None
    assert scheduled == []
    assert empty_calls == ["empty"]
    assert scheduler_instance._refresh_generation == 0
    assert scheduler_instance._pending_refreshes == {}


def test_refresh_all_dedupes_accounts_by_tool_qualified_key() -> None:
    accounts = [
        SimpleNamespace(slug="shared", tool="codex", tray_key="codex:shared"),
        SimpleNamespace(slug="shared", tool="codex", tray_key="codex:shared"),
        SimpleNamespace(slug="shared", tool="claude", tray_key="claude:shared"),
    ]
    scheduler_instance = scheduler.RefreshScheduler(time_fn=lambda: 1000.0)
    scheduled: list[tuple[str, int | None]] = []
    completed: list[str] = []

    generation = scheduler_instance.refresh_all(
        accounts,
        lambda _account: None,
        3600,
        True,
        lambda account, refresh_generation: scheduled.append((account.tray_key, refresh_generation)),
        lambda: completed.append("empty"),
    )

    assert generation == 1
    assert scheduled == [("codex:shared", 1), ("claude:shared", 1)]
    assert completed == []
    assert scheduler_instance._pending_refreshes == {1: 2}
