"""Leaving it running: one job at a time, and a scan a day even if the laptop was asleep."""
from datetime import datetime

import pytest

from scout.locks import Busy, Lock
from scout.projects import Project
from scout.watch import due_for_a_scan, watch

PROJECT = Project(key="terminal", name="Test App", folder="/tmp/app")


def at(day: int, hour: int, minute: int = 0) -> float:
    return datetime(2026, 9, day, hour, minute).timestamp()


# ---- one job at a time ----

def test_a_second_run_is_told_to_wait_its_turn(tmp_path):
    path = tmp_path / "one.lock"
    with Lock(path=path):
        with pytest.raises(Busy) as e:
            with Lock(path=path):
                pass
    assert "already busy" in str(e.value)
    for jargon in ("lock", "flock", "mutex"):
        assert jargon not in str(e.value).lower()


def test_the_lock_is_let_go_afterwards(tmp_path):
    path = tmp_path / "one.lock"
    with Lock(path=path):
        pass
    with Lock(path=path):
        pass                        # no complaint the second time


def test_waiting_gives_up_after_the_time_allowed(tmp_path):
    path = tmp_path / "one.lock"
    ticks = iter([0, 5, 10, 100])
    with Lock(path=path):
        with pytest.raises(Busy):
            with Lock(path=path, wait_s=30, sleep=lambda s: None, clock=lambda: next(ticks)):
                pass


# ---- a scan a day, by the clock rather than a timer ----

def test_nothing_happens_before_the_hour(store):
    assert not due_for_a_scan(store, PROJECT, at(6, 7, 30), hour=8, minute=15)
    assert due_for_a_scan(store, PROJECT, at(6, 8, 15), hour=8, minute=15)


def test_a_laptop_that_was_asleep_catches_up(store):
    """The hour passed while the machine was off; opening it later must still scan."""
    assert due_for_a_scan(store, PROJECT, at(6, 14, 0), hour=8, minute=15)


def test_it_does_not_scan_twice_in_a_day(store):
    import time
    run = store.start_run("scan:terminal")
    store.finish_run(run, True, {"project": "terminal"})
    # hour 0 so only the "already done today" part of the decision is under test
    assert not due_for_a_scan(store, PROJECT, time.time(), hour=0, minute=0)


# ---- the loop itself ----

def run_once(store, *, now, scanned, polled, **kw):
    watch([PROJECT], store, scanner=lambda st, ps: scanned.append([p.key for p in ps]),
          poller=lambda st: polled.append(1), say=lambda m: None,
          now=lambda: now, sleep=lambda s: None, once=True, **kw)


def test_a_pass_looks_for_ideas_and_gets_on_with_the_work(store):
    scanned, polled = [], []
    run_once(store, now=at(6, 9, 0), scanned=scanned, polled=polled)
    assert scanned == [["terminal"]] and polled == [1]


def test_the_work_carries_on_even_when_it_is_not_time_to_look_for_ideas(store):
    scanned, polled = [], []
    run_once(store, now=at(6, 6, 0), scanned=scanned, polled=polled)
    assert scanned == [] and polled == [1]


def test_a_pass_steps_aside_when_something_else_is_running(store, tmp_path, monkeypatch):
    import scout.watch as watchmod
    lock_file = tmp_path / "busy.lock"
    monkeypatch.setattr(watchmod, "Lock", lambda **kw: Lock(path=lock_file, **kw))
    scanned, polled = [], []
    with Lock(path=lock_file):
        run_once(store, now=at(6, 9, 0), scanned=scanned, polled=polled)
    assert scanned == [] and polled == []      # skipped quietly, not crashed
