import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:drift/native.dart'; import 'package:motorcycle_dash/database/database.dart' hide TelemetrySample; import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:motorcycle_dash/models/recording_health_status.dart'; import 'package:motorcycle_dash/services/telemetry_service.dart'; import 'package:motorcycle_dash/services/location_service.dart'; import 'package:motorcycle_dash/services/sensor_fusion_service.dart'; import 'package:motorcycle_dash/services/compass_service.dart'; import 'package:motorcycle_dash/services/audio_alert_service.dart'; import 'package:motorcycle_dash/services/field_test/field_test_logger.dart'; import 'package:motorcycle_dash/services/validation/telemetry_validator.dart'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; class MockAudioAlertService extends Fake implements AudioAlertService { final List spokenMessages = []; @override Future speak(String text, {bool force = false}) async { spokenMessages.add(text); } } class FakeTelemetryService extends TelemetryService { final StreamController _controller = StreamController.broadcast(); FakeTelemetryService( super.loc, super.fusion, super.compass, super.logger, super.validator, ); @override Stream get telemetryStream => _controller.stream; void emitSample(TelemetrySample sample) { _controller.add(sample); } @override void dispose() { _controller.close(); } } class _NullLocationService extends Fake implements LocationService { @override void Function(Position position, bool accepted, String? rejectionReason)? onGpsDiagnostic; @override Stream getFilteredPositionStream() => const Stream.empty(); } class _NullSensorFusionService extends Fake implements SensorFusionService { @override Stream get leanAngleStream => const Stream.empty(); @override Stream get pitchAngleStream => const Stream.empty(); @override Stream get gForceStream => const Stream.empty(); } class _NullCompassService extends Fake implements CompassService { @override Stream get headingStream => const Stream.empty(); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late AppDatabase db; late FakeTelemetryService fakeTelemetry; late MockAudioAlertService mockAudio; late FieldTestLogger logger; late TelemetryValidator validator; late SharedPreferences prefs; setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); db = AppDatabase.forTesting(NativeDatabase.memory()); logger = FieldTestLogger(db); validator = TelemetryValidator(); fakeTelemetry = FakeTelemetryService( _NullLocationService(), _NullSensorFusionService(), _NullCompassService(), logger, validator, ); mockAudio = MockAudioAlertService(); }); tearDown(() async { fakeTelemetry.dispose(); await db.close(); }); test('Physical Failure Regression: Starting ride creates DB session, active health, and logs lifecycle', () async { final container = ProviderContainer( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), databaseProvider.overrideWithValue(db), telemetryServiceProvider.overrideWithValue(fakeTelemetry), audioAlertServiceProvider.overrideWithValue(mockAudio), fieldTestLoggerProvider.overrideWithValue(logger), ], ); addTearDown(container.dispose); // Initial state: Idle var session = container.read(rideStateMachineProvider); expect(session.state, equals(RideState.idle)); expect(session.dbId, isNull); final notifier = container.read(rideStateMachineProvider.notifier); await notifier.startRide(); // Verify state transitioned to RIDING and received a valid DB ID! session = container.read(rideStateMachineProvider); expect(session.state, equals(RideState.riding)); expect(session.dbId, isNotNull); expect(session.healthStatus, equals(RecordingHealthStatus.recordingActive)); // Emit 10 telemetry samples for (int i = 0; i < 10; i++) { fakeTelemetry.emitSample( TelemetrySample( timestamp: DateTime.now(), latitude: 34.2570 + (i * 0.001), longitude: -85.1647 + (i * 0.001), speed: 45.0 + i, heading: 90.0, leanAngle: 12.0, longitudinalG: 0.2, lateralG: 0.1, gpsAccuracy: 4.0, ), ); } await Future.delayed(const Duration(milliseconds: 100)); // Stop session await notifier.finishRide(); await logger.flush(); // Verify ride in SQLite database is non-empty and properly finalized final rides = await db.select(db.rides).get(); expect(rides.length, equals(1)); expect(rides.first.endTime, isNotNull); expect(rides.first.topSpeedMph, greaterThanOrEqualTo(45.0)); // Verify field test events captured lifecycle final events = await db.select(db.fieldTestEvents).get(); // print('Captured events: ${events.map((e) => e.eventType).toList()}'); expect(events.isNotEmpty, isTrue); expect(events.any((e) => e.eventType.contains('START') || e.eventType.contains('RIDE')), isTrue); }); test('Motion Safeguard: Moving vehicle auto-promotes READY state to active recording', () async { final container = ProviderContainer( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), databaseProvider.overrideWithValue(db), telemetryServiceProvider.overrideWithValue(fakeTelemetry), audioAlertServiceProvider.overrideWithValue(mockAudio), fieldTestLoggerProvider.overrideWithValue(logger), ], ); addTearDown(container.dispose); final notifier = container.read(rideStateMachineProvider.notifier); notifier.setReady(); var session = container.read(rideStateMachineProvider); expect(session.state, equals(RideState.ready)); expect(session.dbId, isNull); // Emit movement sample (speed = 25 MPH) fakeTelemetry.emitSample( TelemetrySample( timestamp: DateTime.now(), latitude: 34.2570, longitude: -85.1647, speed: 25.0, heading: 90.0, gpsAccuracy: 3.0, ), ); await Future.delayed(const Duration(milliseconds: 50)); // Verify auto-start promoted to RIDING with assigned dbId session = container.read(rideStateMachineProvider); expect(session.state, equals(RideState.riding)); expect(session.dbId, isNotNull); }); test('Recording Health Contract validates prerequisites strictly', () { const armed = RecordingHealthContract( status: RecordingHealthStatus.recordingArmed, isForegroundServiceAlive: false, isTelemetryPipelineActive: false, ); expect(armed.canReportRecording, isFalse); const active = RecordingHealthContract( status: RecordingHealthStatus.recordingActive, isForegroundServiceAlive: true, isTelemetryPipelineActive: true, isAtLeastOneSensorActive: true, isDatabasePersistenceOperational: true, isValidSessionId: true, ); expect(active.canReportRecording, isTrue); }); }