import 'dart:async'; import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:motorcycle_dash/database/database.dart' hide TelemetrySample; import 'package:motorcycle_dash/features/dashboard/widgets/performance_summary.dart'; import 'package:motorcycle_dash/features/performance/models/performance_launch_models.dart'; import 'package:motorcycle_dash/features/performance/performance_providers.dart'; import 'package:motorcycle_dash/features/performance/performance_timer_service.dart'; import 'package:motorcycle_dash/features/performance/services/monotonic_clock.dart'; import 'package:motorcycle_dash/features/performance/services/performance_launch_state_machine.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; group('PerformanceTimerService Unarmed Isolation', () { late AppDatabase db; late StreamController controller; PerformanceTimerService? timer; setUp(() { db = AppDatabase.forTesting(NativeDatabase.memory()); controller = StreamController.broadcast(); }); tearDown(() async { timer?.dispose(); await controller.close(); await db.close(); }); test('initial state is idle and unarmed', () { timer = PerformanceTimerService(db, controller.stream); expect(timer!.state.state, PerformanceState.idle); expect(timer!.state.isArmed, isFalse); }); test('unarmed timer ignores stationary GPS and subsequent movement (no auto-ready/auto-launch)', () async { timer = PerformanceTimerService(db, controller.stream); final base = DateTime.utc(2026, 10, 12, 12, 0, 0); // Stationary sample arrives on app boot controller.add(TelemetrySample(timestamp: base, speed: 0.0, gpsAgeMs: 0)); await Future.delayed(const Duration(milliseconds: 10)); // MUST remain idle and unarmed expect(timer!.state.state, PerformanceState.idle); expect(timer!.state.isArmed, isFalse); // GPS noise / drift / walk arrives (1.2 MPH) controller.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 200)), speed: 1.2, gpsAgeMs: 0)); await Future.delayed(const Duration(milliseconds: 10)); // MUST NOT start timing! expect(timer!.state.state, PerformanceState.idle); expect(timer!.state.isArmed, isFalse); expect(timer!.state.currentElapsedSeconds, isNull); }); test('explicit setTarget arms the timer and enables launch on movement', () async { timer = PerformanceTimerService(db, controller.stream); final base = DateTime.utc(2026, 10, 12, 12, 0, 0); timer!.setTarget(PerformanceTarget.sixty); expect(timer!.state.state, PerformanceState.ready); expect(timer!.state.isArmed, isTrue); // Stationary sample reaffirms ready controller.add(TelemetrySample(timestamp: base, speed: 0.0, gpsAgeMs: 0)); await Future.delayed(const Duration(milliseconds: 10)); expect(timer!.state.state, PerformanceState.ready); // Accelerates past 1.0 MPH -> starts timing controller.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 100)), speed: 3.5, gpsAgeMs: 0)); await Future.delayed(const Duration(milliseconds: 10)); expect(timer!.state.state, PerformanceState.timing); expect(timer!.state.currentElapsedSeconds, isNotNull); }); test('reset clears state back to unarmed idle', () { timer = PerformanceTimerService(db, controller.stream); timer!.setTarget(PerformanceTarget.sixty); expect(timer!.state.isArmed, isTrue); timer!.reset(); expect(timer!.state.state, PerformanceState.idle); expect(timer!.state.isArmed, isFalse); }); }); group('PerformanceSummary Widget Presentation', () { late SharedPreferences prefs; setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); }); testWidgets('renders SizedBox.shrink when launch and timer are idle and unarmed', (tester) async { final db = AppDatabase.forTesting(NativeDatabase.memory()); final controller = StreamController.broadcast(); addTearDown(() async { await controller.close(); await db.close(); }); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), databaseProvider.overrideWithValue(db), ], child: const MaterialApp( home: Scaffold( body: PerformanceSummary(), ), ), ), ); await tester.pump(); // Should not find timer icon or trophy icon expect(find.byIcon(Icons.timer), findsNothing); expect(find.byIcon(Icons.emoji_events_outlined), findsNothing); expect(find.textContaining('0-60'), findsNothing); }); testWidgets('renders active timing chip when performanceLaunchProvider is launched', (tester) async { final db = AppDatabase.forTesting(NativeDatabase.memory()); final clock = FakeMonotonicClock(); final attempt = const PerformanceLaunchAttempt( state: PerformanceLaunchState.launched, target: PerformanceLaunchTarget.sixty, currentElapsedSeconds: 2.34, ); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), databaseProvider.overrideWithValue(db), performanceLaunchProvider.overrideWith((ref) { return PerformanceLaunchStateMachine( db: db, telemetryStream: const Stream.empty(), clock: clock, )..state = attempt; }), ], child: const MaterialApp( home: Scaffold( body: PerformanceSummary(), ), ), ), ); await tester.pump(); expect(find.byIcon(Icons.timer), findsOneWidget); expect(find.text('0-60: 2.34s'), findsOneWidget); }); testWidgets('renders completed result chip when performanceLaunchProvider has lastResult', (tester) async { final db = AppDatabase.forTesting(NativeDatabase.memory()); final clock = FakeMonotonicClock(); final result = PerformanceLaunchResult( mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty, accelerationTimeSec: 3.42, topSpeedReachedMph: 61.2, timestamp: DateTime.utc(2026, 10, 12, 12, 0, 0), ); final attempt = PerformanceLaunchAttempt( state: PerformanceLaunchState.completed, target: PerformanceLaunchTarget.sixty, lastResult: result, ); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), databaseProvider.overrideWithValue(db), performanceLaunchProvider.overrideWith((ref) { return PerformanceLaunchStateMachine( db: db, telemetryStream: const Stream.empty(), clock: clock, )..state = attempt; }), ], child: const MaterialApp( home: Scaffold( body: PerformanceSummary(), ), ), ), ); await tester.pump(); expect(find.byIcon(Icons.emoji_events_outlined), findsOneWidget); expect(find.text('0-60: 3.42s'), findsOneWidget); }); testWidgets('tapping timing chip pushes /launch route', (tester) async { final db = AppDatabase.forTesting(NativeDatabase.memory()); final clock = FakeMonotonicClock(); final attempt = const PerformanceLaunchAttempt( state: PerformanceLaunchState.launched, target: PerformanceLaunchTarget.sixty, currentElapsedSeconds: 1.50, ); var navigatedToLaunch = false; final router = GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => const Scaffold(body: PerformanceSummary()), ), GoRoute( path: '/launch', builder: (context, state) { navigatedToLaunch = true; return const Scaffold(body: Text('Launch Screen')); }, ), ], ); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), databaseProvider.overrideWithValue(db), performanceLaunchProvider.overrideWith((ref) { return PerformanceLaunchStateMachine( db: db, telemetryStream: const Stream.empty(), clock: clock, )..state = attempt; }), ], child: MaterialApp.router( routerConfig: router, ), ), ); await tester.pumpAndSettle(); expect(find.byIcon(Icons.timer), findsOneWidget); await tester.tap(find.byType(PerformanceSummary)); await tester.pumpAndSettle(); expect(navigatedToLaunch, isTrue); }); }); }