"""PollScheduler robustness: one-off cancellation + surviving link death."""
import os
import sys
import threading
import time

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from obdcore import PidRegistry, TimeSeriesStore, PollScheduler, load_default
from obdcore.mock import MockLink


def _sched(link=None):
    prof = load_default()
    return PollScheduler(link or MockLink(clock=time.time), PidRegistry(prof),
                         TimeSeriesStore(), clock=time.time)


class _RaisingLink(MockLink):
    def read_m01(self, pid, nbytes, timeout=0.6):
        raise OSError("device disconnected")


def test_oneoff_cancel_on_timeout():
    s = _sched()
    s._running = True                 # pretend a poll thread is up but not servicing
    ran = []
    try:
        s.run_oneoff(lambda: ran.append(1), timeout=0.05)
        raise AssertionError("should have timed out")
    except TimeoutError as e:
        assert "cancelled" in str(e), e
    s._drain_oneoffs()                # the cancelled job must NOT execute later
    assert ran == [], "cancelled one-off fired late — ghost command"
    print("  timed-out one-off is cancelled, never runs late: OK")


def test_oneoff_freed_when_thread_dies():
    s = _sched()
    s._running = True
    got = []

    def submit():
        try:
            s.run_oneoff(lambda: None, timeout=5.0)
        except Exception as e:
            got.append(e)

    t = threading.Thread(target=submit); t.start()
    time.sleep(0.05)
    s._fail_pending_oneoffs(RuntimeError("link died"))   # simulate thread death
    t.join(timeout=1.0)
    assert got and "link died" in str(got[0]), got
    print("  blocked one-off freed immediately on thread death: OK")


def test_loop_survives_link_death_and_reports():
    errs = []
    s = PollScheduler(_RaisingLink(clock=time.time), PidRegistry(load_default()),
                      TimeSeriesStore(), clock=time.time, on_error=lambda e: errs.append(e))
    s.subscribe("RPM", 5)
    s._running = True
    s._loop()                          # the raising read propagates -> caught, not fatal
    assert not s._running, "thread should stop, not spin"
    assert errs and isinstance(errs[0], OSError), errs
    print("  poll loop catches transport death + fires on_error: OK")


if __name__ == "__main__":
    test_oneoff_cancel_on_timeout()
    test_oneoff_freed_when_thread_dies()
    test_loop_survives_link_death_and_reports()
    print("\nALL SCHEDULER TESTS PASS")
