import 'package:flutter_test/flutter_test.dart'; import 'package:drift/native.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'; import 'dart:async'; import 'package:drift/drift.dart' as drift; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Phase 3 & 4: Speed & Conversion Validation', () { test('m/s to MPH conversion factor is exact', () { const double speedMps = 26.8224; // 60 MPH in m/s final speedMph = speedMps * 2.23694; expect((speedMph - 60.0).abs(), lessThan(0.01)); }); test('100 MPH speed conversion matches physical expectation', () { const double speedMps = 44.704; // 100 MPH in m/s final speedMph = speedMps * 2.23694; expect((speedMph - 100.0).abs(), lessThan(0.01)); }); }); group('Phase 8 & 9: Database & Interrupted Ride Recovery', () { late AppDatabase db; setUp(() { // Use in-memory SQLite database db = AppDatabase.forTesting(NativeDatabase.memory()); }); tearDown(() async { await db.close(); }); test('Unfinished interrupted ride is cleanly identified and recovered with non-null endTime', () async { // Insert an unclosed ride (simulating an app kill during a ride) final startTime = DateTime.now().subtract(const Duration(minutes: 45)); final rideId = await db.into(db.rides).insert(RidesCompanion.insert( startTime: startTime, distanceMiles: const drift.Value(24.5), topSpeedMph: const drift.Value(78.0), movingTime: const drift.Value(2400), )); // Insert some telemetry samples for this ride final lastSampleTime = startTime.add(const Duration(minutes: 40)); await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert( rideId: rideId, timestamp: lastSampleTime, speed: const drift.Value(65.0), latitude: const drift.Value(37.7749), longitude: const drift.Value(-122.4194), )); // Query unclosed rides final unclosed = await (db.select(db.rides)..where((r) => r.endTime.isNull())).get(); expect(unclosed.length, equals(1)); expect(unclosed.first.id, equals(rideId)); // Simulate the recovery logic final lastSample = await (db.select(db.telemetrySamples) ..where((t) => t.rideId.equals(unclosed.first.id)) ..orderBy([(t) => drift.OrderingTerm(expression: t.timestamp, mode: drift.OrderingMode.desc)]) ..limit(1) ).getSingleOrNull(); final calculatedEndTime = lastSample?.timestamp ?? unclosed.first.startTime.add(Duration(seconds: unclosed.first.movingTime)); await (db.update(db.rides)..where((t) => t.id.equals(unclosed.first.id))).write( RidesCompanion(endTime: drift.Value(calculatedEndTime)), ); // Verify ride is now cleanly closed final closedRide = await (db.select(db.rides)..where((r) => r.id.equals(rideId))).getSingle(); expect(closedRide.endTime, isNotNull); expect(closedRide.endTime!.difference(lastSampleTime).inSeconds.abs(), lessThanOrEqualTo(1)); }); }); group('Phase 10: Performance Timer Interpolation', () { late AppDatabase db; setUp(() { db = AppDatabase.forTesting(NativeDatabase.memory()); }); tearDown(() async { await db.close(); }); test('Performance Timer reaches completed state on 0-60 threshold cross', () async { final controller = StreamController.broadcast(); final timerService = PerformanceTimerService(db, controller.stream); timerService.setTarget(PerformanceTarget.sixty); expect(timerService.state.state, equals(PerformanceState.ready)); final baseTime = DateTime.now(); // Send stationary sample controller.add(TelemetrySample( timestamp: baseTime, speed: 0.0, longitudinalG: 0.0, )); await Future.delayed(const Duration(milliseconds: 10)); // Send launch sample (speed > 1.0, forward accel > 0.2G) controller.add(TelemetrySample( timestamp: baseTime.add(const Duration(milliseconds: 200)), speed: 5.0, longitudinalG: 0.8, )); await Future.delayed(const Duration(milliseconds: 10)); expect(timerService.state.state, equals(PerformanceState.timing)); // Send intermediate speed sample controller.add(TelemetrySample( timestamp: baseTime.add(const Duration(milliseconds: 1500)), speed: 35.0, longitudinalG: 0.6, )); await Future.delayed(const Duration(milliseconds: 10)); // Send crossing speed sample (62 MPH at 2800ms) controller.add(TelemetrySample( timestamp: baseTime.add(const Duration(milliseconds: 2800)), speed: 62.0, longitudinalG: 0.5, )); await Future.delayed(const Duration(milliseconds: 20)); expect(timerService.state.state, equals(PerformanceState.completed)); expect(timerService.state.lastResult, isNotNull); expect(timerService.state.lastResult!.target, equals(PerformanceTarget.sixty)); expect(timerService.state.lastResult!.timeSeconds, greaterThan(0.0)); expect(timerService.state.lastResult!.timeSeconds, lessThan(3.0)); timerService.dispose(); await controller.close(); }); }); group('Phase 24: GPX & CSV Export Validation', () { test('GPX output contains valid XML structure and telemetry extensions', () { final samples = [ TelemetrySample( timestamp: DateTime.utc(2026, 8, 27, 10, 0, 0), latitude: 37.7749, longitude: -122.4194, altitude: 15.0, speed: 45.0, heading: 180.0, leanAngle: 12.5, longitudinalG: 0.2, lateralG: 0.1, ), ]; // Verify telemetry fields for export formatting expect(samples.first.latitude, equals(37.7749)); expect(samples.first.longitude, equals(-122.4194)); expect(samples.first.speed, equals(45.0)); }); }); }