from datetime import date from app import db as d from app.config import get_settings from app.services import compute_service_due def _conn(): settings = get_settings() settings.data_dir.mkdir(parents=True, exist_ok=True) d.init_schema(settings.resolved_db_path) return d.connect(settings.resolved_db_path) def _mk_bike(conn, interval): return d.create_bike(conn, "B", interval) def _event(conn, bike_id, type, day, mileage, cost=None): d.create_event(conn, bike_id, type, day, mileage, cost, "n") def test_no_data_when_no_service(): conn = _conn() bike_id = _mk_bike(conn, 5000) _event(conn, bike_id, "tyre", date(2026, 1, 1), 10000) due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "no-data" conn.close() def test_ok_when_under_interval(): conn = _conn() bike_id = _mk_bike(conn, 5000) _event(conn, bike_id, "service", date(2026, 1, 1), 10000) _event(conn, bike_id, "tyre", date(2026, 6, 1), 12000) due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "ok" assert due.miles_since == 2000 assert due.miles_until_due == 3000 conn.close() def test_overdue_when_over_interval(): conn = _conn() bike_id = _mk_bike(conn, 5000) _event(conn, bike_id, "service", date(2026, 1, 1), 10000) _event(conn, bike_id, "other", date(2026, 6, 1), 16000) due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "overdue" assert due.miles_since == 6000 assert due.miles_until_due == -1000 conn.close() def test_exactly_at_interval_is_due_soon(): conn = _conn() bike_id = _mk_bike(conn, 5000) _event(conn, bike_id, "service", date(2026, 1, 1), 10000) _event(conn, bike_id, "other", date(2026, 6, 1), 15000) due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "due-soon" assert due.miles_until_due == 0 conn.close() def test_within_lead_time_is_due_soon(): conn = _conn() bike_id = _mk_bike(conn, 5000) _event(conn, bike_id, "service", date(2026, 1, 1), 10000) _event(conn, bike_id, "other", date(2026, 6, 1), 14900) # 100 mi left due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "due-soon" assert due.miles_until_due == 100 conn.close() def test_just_outside_lead_time_is_ok(): conn = _conn() bike_id = _mk_bike(conn, 5000) _event(conn, bike_id, "service", date(2026, 1, 1), 10000) _event(conn, bike_id, "other", date(2026, 6, 1), 14750) # 250 mi left due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "ok" assert due.miles_until_due == 250 conn.close() def test_no_interval_set_is_no_data(): conn = _conn() bike_id = _mk_bike(conn, None) _event(conn, bike_id, "service", date(2026, 1, 1), 10000) due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "no-data" conn.close() def test_latest_service_used_not_earliest(): conn = _conn() bike_id = _mk_bike(conn, 5000) _event(conn, bike_id, "service", date(2025, 1, 1), 8000) _event(conn, bike_id, "service", date(2026, 1, 1), 10000) # most recent _event(conn, bike_id, "other", date(2026, 6, 1), 14000) due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "ok" assert due.miles_since == 4000 conn.close() def test_service_without_mileage_is_ignored(): conn = _conn() bike_id = _mk_bike(conn, 5000) _event(conn, bike_id, "service", date(2026, 1, 1), None) # no mileage due = compute_service_due(conn, d.get_bike(conn, bike_id)) assert due.status == "no-data" assert due.last_service is None conn.close()