"""Routine scheduler: cron parsing, reload → jobs, and fire_routine dispatch to
a connected session (or the no-session log path). APScheduler's real CronTrigger
is used (offline), but the scheduler backend is a FakeSched so no loop/timers."""
import main
import phone_store
import scheduler
import tools


# --- parse_cron ------------------------------------------------------------
def test_parse_cron_daily():
    c = scheduler.parse_cron({"type": "time", "at": "07:00"})
    assert c == {"hour": 7, "minute": 0, "day_of_week": None}


def test_parse_cron_specific_days():
    c = scheduler.parse_cron({"type": "both", "at": "18:30",
                              "days": ["Mon", "wednesday"], "place": "office"})
    assert c["hour"] == 18 and c["minute"] == 30
    assert c["day_of_week"] == "mon,wed"


def test_parse_cron_rejects_place_only_and_bad_time():
    assert scheduler.parse_cron({"type": "place", "place": "gym"}) is None
    assert scheduler.parse_cron({"type": "time", "at": "nope"}) is None
    assert scheduler.parse_cron({"type": "time", "at": "25:00"}) is None
    assert scheduler.parse_cron({}) is None


# --- RoutineScheduler.reload ----------------------------------------------
class FakeJob:
    def __init__(self, store, jid):
        self.id = jid
        self._store = store

    def remove(self):
        self._store[:] = [j for j in self._store if j is not self]


class FakeSched:
    def __init__(self):
        self.jobs = []
        self.running = False

    def start(self):
        self.running = True

    def get_jobs(self):
        return list(self.jobs)

    def add_job(self, func, trigger=None, args=None, id=None, **kw):
        job = FakeJob(self.jobs, id)
        self.jobs.append(job)
        return job

    def shutdown(self, wait=False):
        self.running = False


async def test_reload_schedules_only_time_routines():
    routines = [
        {"name": "brief", "trigger": {"type": "time", "at": "07:00"},
         "action": {"type": "speak_brief", "brief": "tasks"}},
        {"name": "evening", "trigger": {"type": "both", "at": "20:00",
                                        "place": "home"},
         "action": {"type": "say_text", "text": "hi"}},
        {"name": "atwork", "trigger": {"type": "place", "place": "office"},
         "action": {"type": "speak_brief", "brief": "tasks"}},
    ]

    async def list_routines():
        return routines

    fake = FakeSched()
    sched = scheduler.RoutineScheduler(dispatch=None,
                                       list_routines=list_routines, sched=fake)
    n = await sched.start()
    assert n == 2                              # time + both, not place-only
    assert len(fake.jobs) == 2
    # a reload rebuilds cleanly (no accumulation)
    assert await sched.reload() == 2
    assert len(fake.jobs) == 2


async def test_reload_survives_storage_error():
    async def boom():
        raise RuntimeError("memory down")
    fake = FakeSched()
    sched = scheduler.RoutineScheduler(dispatch=None, list_routines=boom,
                                       sched=fake)
    assert await sched.start() == 0
    assert fake.jobs == []


# --- fire_routine dispatch -------------------------------------------------
class FakeSession:
    def __init__(self, user):
        self.user = user
        self.delivered = []

    async def _deliver_proactive(self, injected):
        self.delivered.append(injected)


async def test_fire_routine_speaks_to_connected_session(monkeypatch):
    async def fake_tasks(_args):
        return "- Call the supplier"
    monkeypatch.setattr(main.tools, "_tasks_list", fake_tasks)

    user = tools._group()
    sess = FakeSession(user)
    main.register_session(sess)
    try:
        await main.fire_routine({
            "name": "morning brief",
            "trigger": {"type": "time", "at": "07:00"},
            "action": {"type": "speak_brief", "brief": "tasks"}})
    finally:
        main.unregister_session(sess)
    assert sess.delivered, "a connected session must receive the brief"
    assert "Call the supplier" in sess.delivered[0]
    assert "morning brief" in sess.delivered[0]


async def test_fire_routine_no_session_just_logs(caplog):
    # no session registered for this user → must not raise
    await main.fire_routine({
        "name": "orphan", "trigger": {"type": "time", "at": "07:00"},
        "action": {"type": "say_text", "text": "hello"}})
    # (nothing to assert beyond "did not raise" + TODO(fcm) log line)


async def test_reschedule_hook_reloads(monkeypatch):
    called = []

    async def list_routines():
        return []
    fake = FakeSched()
    sched = scheduler.RoutineScheduler(dispatch=None,
                                       list_routines=list_routines, sched=fake)
    await sched.start()
    main._SCHED = sched
    orig = sched.reload

    async def spy():
        called.append(1)
        return await orig()
    sched.reload = spy
    await main._reschedule()
    assert called == [1]
