import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/lean_angle_indicator.dart'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:motorcycle_dash/services/telemetry_service.dart'; import 'package:motorcycle_dash/services/sensor_fusion_service.dart'; import 'package:motorcycle_dash/services/calibration_service.dart'; void main() { testWidgets('LeanAngleIndicator tap while moving (> 3.0 MPH) blocks calibration with warning', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 12.0, speed: 25.0, // Moving at 25 MPH ); await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.riding), ), ), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); // Verify indicator rendered expect(find.byKey(const ValueKey('lean_angle_indicator_tap')), findsOneWidget); // Tap on the indicator await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pump(); // Verify warning snackbar is shown expect(find.byKey(const ValueKey('lean_calibrate_moving_warning')), findsOneWidget); expect(find.textContaining('Cannot calibrate while moving (> 3.0 MPH)'), findsOneWidget); // Calibration dialog must NOT appear expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsNothing); }); testWidgets('LeanAngleIndicator tap while stationary (< 3.0 MPH) prompts confirmation and calibrates upright', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 0.0, speed: 0.0, // Stationary ); var calibrated = false; await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.idle), ), ), sensorFusionServiceProvider.overrideWith((ref) => SensorFusionServiceMock(() { calibrated = true; })), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); // Tap on indicator await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pumpAndSettle(); // Verify dialog appears expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsOneWidget); expect(find.byKey(const ValueKey('confirm_lean_calibrate_button')), findsOneWidget); // Confirm calibration await tester.tap(find.byKey(const ValueKey('confirm_lean_calibrate_button'))); await tester.pumpAndSettle(); expect(calibrated, isTrue, reason: 'Must invoke calibrateUpright() on SensorFusionService'); expect(find.byKey(const ValueKey('lean_calibrate_success_snackbar')), findsOneWidget); }); testWidgets('LeanAngleIndicator tap while stationary (< 3.0 MPH) during active RideState.riding allows calibration', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 0.0, speed: 0.0, // Stationary at traffic stop during ride ); await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.riding), // Actively recording ), ), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); // Tap on indicator await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pumpAndSettle(); // Verify dialog appears because motorcycle is stationary even though ride is recording expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsOneWidget); expect(find.byKey(const ValueKey('confirm_lean_calibrate_button')), findsOneWidget); }); testWidgets('LeanAngleIndicator tap while tilted on kickstand (> 12.0°) requires explicit upright confirmation and blocks unconfirmed calibration', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 15.0, // Resting on kickstand speed: 0.0, ); var calibrated = false; await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.idle), ), ), sensorFusionServiceProvider.overrideWith((ref) => SensorFusionServiceMock(() { calibrated = true; })), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pumpAndSettle(); // Verify recovery dialog appears with tilted warning container expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsOneWidget); expect(find.byKey(const ValueKey('lean_calibrate_tilted_warning_container')), findsOneWidget); // Attempt calibration without checking the upright confirmation box await tester.tap(find.byKey(const ValueKey('confirm_lean_calibrate_button'))); await tester.pump(); // Calibration must NOT occur and unconfirmed warning snackbar must be shown expect(calibrated, isFalse); expect(find.byKey(const ValueKey('lean_calibrate_tilted_warning')), findsOneWidget); expect(find.textContaining('Motorcycle is tilted (15°). Hold the bike upright'), findsOneWidget); }); testWidgets('Right-pinned lean gauge defect (42.0° right while upright) recovers via stationary upright confirmation', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 42.0, // Right-pinned defect on Pixel 8a while Yamaha R6 is upright speed: 0.0, ); var calibrated = false; await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.idle), ), ), sensorFusionServiceProvider.overrideWith((ref) => SensorFusionServiceMock(() { calibrated = true; })), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); // Verify gauge is showing tilted / right-pinned value expect(find.text('42°'), findsOneWidget); // Tap indicator to initiate recovery await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pumpAndSettle(); // Verify recovery dialog opened expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsOneWidget); expect(find.text('Calibrate Lean Angle (Upright Recovery)'), findsOneWidget); expect(find.byKey(const ValueKey('confirm_physically_upright_checkbox')), findsOneWidget); // Check the confirmation box confirming bike is physically upright await tester.tap(find.byKey(const ValueKey('confirm_physically_upright_checkbox'))); await tester.pumpAndSettle(); // Tap recalibrate button await tester.tap(find.byKey(const ValueKey('confirm_lean_calibrate_button'))); await tester.pumpAndSettle(); // Verify calibration succeeded expect(calibrated, isTrue, reason: 'Right-pinned gauge must successfully recalibrate upright'); expect(find.byKey(const ValueKey('lean_calibrate_success_snackbar')), findsOneWidget); }); testWidgets('LeanAngleIndicator tap while acceleration is unstable (> 0.25G) blocks calibration with warning', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 0.0, speed: 0.0, longitudinalG: 0.35, // High vibration or dynamic shake ); await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.idle), ), ), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pump(); // Verify unstable warning snackbar is shown expect(find.byKey(const ValueKey('lean_calibrate_unstable_warning')), findsOneWidget); expect(find.textContaining('Sensors are unstable'), findsOneWidget); // Calibration dialog must NOT appear expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsNothing); }); } class MockRideStateMachine extends StateNotifier implements RideStateMachine { MockRideStateMachine(super.initialState); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class SensorFusionServiceMock extends SensorFusionService { final VoidCallback onCalibrate; SensorFusionServiceMock(this.onCalibrate) : super(CalibrationService()); @override Future calibrateUpright({int? motorcycleId}) async { onCalibrate(); } }