import 'dart:async'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/database/database.dart' hide TelemetrySample; import 'package:motorcycle_dash/features/performance/performance_timer_service.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart'; void main() { final base = DateTime.utc(2026, 9, 19); test( 'normal READY launch retains interpolation fallback and null-speed handling', () async { final db = AppDatabase.forTesting(NativeDatabase.memory()); final controller = StreamController(sync: true); final timer = PerformanceTimerService(db, controller.stream); addTearDown(() async { timer.dispose(); await controller.close(); await db.close(); }); timer.setTarget(PerformanceTarget.sixty); for (final age in [null, 0, 4001]) { controller.add(TelemetrySample(timestamp: base, gpsAgeMs: age)); expect(timer.state.state, PerformanceState.ready); } // No previous speed sample: launch time falls back to this timestamp. controller.add(TelemetrySample(timestamp: base, speed: 5, gpsAgeMs: 0)); expect(timer.state.state, PerformanceState.timing); for (final age in [null, 0, 4001]) { controller.add( TelemetrySample( timestamp: base.add(const Duration(milliseconds: 100)), gpsAgeMs: age, ), ); expect(timer.state.state, PerformanceState.timing); } controller.add( TelemetrySample( timestamp: base.add(const Duration(seconds: 3)), speed: 60, gpsAgeMs: 0, ), ); expect(timer.state.state, PerformanceState.completed); final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); expect(runs.single.timeMs, 3000); }, ); test('stale READY requires new stationary evidence before launch', () async { final db = AppDatabase.forTesting(NativeDatabase.memory()); final controller = StreamController(sync: true); final timer = PerformanceTimerService(db, controller.stream); addTearDown(() async { timer.dispose(); await controller.close(); await db.close(); }); void emit(int ms, double speed, int age) { controller.add( TelemetrySample( timestamp: base.add(Duration(milliseconds: ms)), speed: speed, gpsAgeMs: age, ), ); } timer.setTarget(PerformanceTarget.sixty); emit(0, 0, 0); expect(timer.state.state, PerformanceState.ready); emit(4000, 0, 4000); expect(timer.state.state, PerformanceState.ready); emit(4001, 0, 4001); expect(timer.state.state, PerformanceState.idle); emit(5000, 30, 5000); emit(6000, 40, 0); expect(timer.state.state, PerformanceState.idle); emit(9000, 65, 0); expect(timer.state.state, PerformanceState.idle); expect(await db.select(db.performanceRuns).get(), isEmpty); emit(10000, 0, 0); expect(timer.state.state, PerformanceState.ready); emit(10200, 5, 0); expect(timer.state.state, PerformanceState.timing); emit(13000, 65, 0); expect(timer.state.state, PerformanceState.completed); final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); expect(runs.single.timeMs, closeTo(2727, 1)); }); for (final coordinates in <(double?, double?)>[ (null, null), (null, 0), (0, null), (double.nan, 0), (0, double.nan), (double.infinity, 0), (0, double.negativeInfinity), (90.1, 0), (-90.1, 0), (0, 180.1), (0, -180.1), ]) { for (final metadata in ['absent', 'age', 'timestamp']) { test('invalid coordinates $coordinates with $metadata are not fresh', () { expect( TelemetrySample( timestamp: base, latitude: coordinates.$1, longitude: coordinates.$2, gpsAgeMs: metadata == 'age' ? 0 : null, gpsTimestamp: metadata == 'timestamp' ? base : null, ).isGpsFresh, isFalse, ); }); } } for (final age in [3999, 4000, 4001]) { for (final useAge in [true, false]) { test('GPS boundary $age with explicit age $useAge', () { expect( TelemetrySample( timestamp: base, latitude: 0, longitude: 0, gpsAgeMs: useAge ? age : null, gpsTimestamp: useAge ? null : base.subtract(Duration(milliseconds: age)), ).isGpsFresh, age <= 4000, ); }); } } test('valid coordinate limits retain metadata-free compatibility', () { for (final coordinates in [(90.0, 180.0), (-90.0, -180.0), (0.0, 0.0)]) { expect( TelemetrySample( timestamp: base, latitude: coordinates.$1, longitude: coordinates.$2, ).isGpsFresh, isTrue, ); } }); }