import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:drift/native.dart'; import 'package:drift/drift.dart' as drift; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/database/database.dart'; import 'package:motorcycle_dash/services/field_test/field_test_models.dart'; import 'package:motorcycle_dash/services/field_test/field_test_logger.dart'; import 'package:motorcycle_dash/services/field_test/field_test_export_service.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late AppDatabase db; late ProviderContainer container; late FieldTestLogger logger; late FieldTestExportService exportService; setUp(() async { SharedPreferences.setMockInitialValues({}); db = AppDatabase.forTesting(NativeDatabase.memory()); container = ProviderContainer( overrides: [ databaseProvider.overrideWithValue(db), ], ); logger = container.read(fieldTestLoggerProvider); exportService = container.read(fieldTestExportServiceProvider); }); tearDown(() async { container.dispose(); await db.close(); }); group('Field Test Logger - Session Management & Lifecycle', () { test('startSession creates a new session and records environment info', () async { final bike = GarageMotorcycle( id: 1, year: 2024, makeId: 'ducati', makeName: 'Ducati', modelId: 'panigale_v4', modelName: 'Panigale V4', variantId: 's', variantName: 'S', nickname: 'Red Beast', isDefault: true, dateAdded: DateTime.now(), lastUpdated: DateTime.now(), ); final sessionId = await logger.startSession( rideId: 101, bike: bike, rollOffset: 1.5, pitchOffset: -0.8, ); expect(sessionId, isNotNull); expect(logger.isSessionActive, isTrue); expect(logger.activeSessionId, equals(sessionId)); final session = await (db.select(db.fieldTestSessions)..where((s) => s.id.equals(sessionId!))).getSingle(); expect(session.rideId, equals(101)); expect(session.motorcycleMake, equals('Ducati')); expect(session.motorcycleModel, equals('Panigale V4')); expect(session.calibrationRollOffset, equals(1.5)); expect(session.calibrationPitchOffset, equals(-0.8)); expect(session.completed, isFalse); }); test('stopSession finalizes session, flushes data, and calculates health summary', () async { final sessionId = await logger.startSession(rideId: 102); expect(sessionId, isNotNull); // Log some events logger.logEvent( eventType: FieldTestEventType.gpsFix, severity: FieldTestSeverity.debug, message: 'GPS fix acquired', ); logger.logGpsEvent( latitude: 37.7749, longitude: -122.4194, accuracy: 5.0, gpsHealth: 'Healthy', accepted: true, ); await logger.stopSession(); expect(logger.isSessionActive, isFalse); expect(logger.activeSessionId, isNull); final session = await (db.select(db.fieldTestSessions)..where((s) => s.id.equals(sessionId!))).getSingle(); expect(session.completed, isTrue); expect(session.endedAt, isNotNull); expect(session.healthSummaryJson, isNotNull); }); test('Immediate flush occurs on critical severity events', () async { final sessionId = await logger.startSession(rideId: 103); expect(sessionId, isNotNull); logger.logEvent( eventType: FieldTestEventType.loggerError, severity: FieldTestSeverity.critical, message: 'Critical subsystem failure occurred', ); // Critical event should immediately flush to DB without waiting 5s timer final events = await (db.select(db.fieldTestEvents)..where((e) => e.sessionId.equals(sessionId!))).get(); expect(events.any((e) => e.severity == 'critical'), isTrue); }); }); group('Field Test Logger - GPS Diagnostics & Outlier Rejection', () { test('Records accepted and rejected GPS points with explicit rejection reasons', () async { final sessionId = await logger.startSession(rideId: 104); expect(sessionId, isNotNull); // Accepted fix logger.logGpsEvent( latitude: 34.0522, longitude: -118.2437, rawSpeed: 25.0, accuracy: 4.2, gpsHealth: 'Healthy', accepted: true, ); // Rejected fix (impossible speed) logger.logGpsEvent( latitude: 34.0530, longitude: -118.2440, rawSpeed: 120.0, // 268 MPH accuracy: 45.0, gpsHealth: 'Poor', accepted: false, rejectionReason: 'Impossible speed (268.4 MPH > 223 MPH)', ); await logger.flush(); final gpsRecords = await (db.select(db.fieldTestGpsRecords)..where((g) => g.sessionId.equals(sessionId!))).get(); expect(gpsRecords.length, equals(2)); expect(gpsRecords.where((g) => g.accepted).length, equals(1)); expect(gpsRecords.where((g) => !g.accepted).length, equals(1)); expect(gpsRecords.firstWhere((g) => !g.accepted).rejectionReason, contains('Impossible speed')); }); test('Tracks GPS outage duration and recovery time accurately', () async { await logger.startSession(rideId: 105); logger.logGpsSignalLoss(); // Simulate brief delay await Future.delayed(const Duration(milliseconds: 50)); logger.logGpsSignalRecovery(); logger.logGpsBaselineReset(); await logger.flush(); expect(logger.liveLogs.any((l) => l.eventType == FieldTestEventType.gpsLost), isTrue); expect(logger.liveLogs.any((l) => l.eventType == FieldTestEventType.gpsRecovered), isTrue); expect(logger.liveLogs.any((l) => l.eventType == FieldTestEventType.gpsBaselineReset), isTrue); }); }); group('Field Test Logger - IMU & Sensor Fusion Diagnostics', () { test('Detects and logs vibration spikes exceeding delta threshold', () async { final sessionId = await logger.startSession(rideId: 106); expect(sessionId, isNotNull); // Normal IMU sample (delta = 0.05G) logger.logImuDiagnostic( accelX: 0.1, accelY: 0.5, accelZ: 9.8, longitudinalG: 0.52, filteredLongitudinalG: 0.50, ); // Vibration spike (delta = 1.34G > 0.8G threshold) logger.logImuDiagnostic( accelX: 0.2, accelY: 2.14, accelZ: 9.8, longitudinalG: 2.14, filteredLongitudinalG: 0.80, ); await logger.flush(); final imuRecords = await (db.select(db.fieldTestImuRecords)..where((i) => i.sessionId.equals(sessionId!))).get(); expect(imuRecords.length, equals(2)); final spikeEvents = logger.liveLogs.where((l) => l.eventType == FieldTestEventType.imuSpikeFiltered); expect(spikeEvents.length, equals(1)); expect(spikeEvents.first.message, contains('Vibration Spike Filtered')); }); }); group('Field Test Logger - Road Alerts & TTS', () { test('Logs alert reception and TTS speech results', () async { await logger.startSession(rideId: 107); logger.logRoadAlertEvent( type: FieldTestEventType.roadAlertReceived, providerId: 'community_traffic', category: 'Police', title: 'Speed Trap Reported', distanceMeters: 250, ); logger.logTts(success: true, phrase: 'Police report ahead.'); logger.logTts(success: false, phrase: 'Hazard ahead.', error: 'Audio focus lost'); expect(logger.liveLogs.any((l) => l.message.contains('Police')), isTrue); expect(logger.liveLogs.any((l) => l.eventType == FieldTestEventType.ttsSuccess), isTrue); expect(logger.liveLogs.any((l) => l.eventType == FieldTestEventType.ttsFailure), isTrue); }); }); group('Field Test Logger - Safe Retention & Active Protection', () { test('Active session is protected from deletion', () async { final sessionId = await logger.startSession(rideId: 108); expect(sessionId, isNotNull); final deleted = await logger.deleteSession(sessionId!); expect(deleted, isFalse); // Must reject active session deletion final session = await (db.select(db.fieldTestSessions)..where((s) => s.id.equals(sessionId))).getSingleOrNull(); expect(session, isNotNull); }); test('Inactive session can be deleted with cascade cleanup', () async { final sessionId = await logger.startSession(rideId: 109); expect(sessionId, isNotNull); logger.logEvent( eventType: FieldTestEventType.speedChanged, severity: FieldTestSeverity.info, message: 'Speed 45 MPH', ); await logger.stopSession(); final deleted = await logger.deleteSession(sessionId!); expect(deleted, isTrue); final session = await (db.select(db.fieldTestSessions)..where((s) => s.id.equals(sessionId))).getSingleOrNull(); expect(session, isNull); final events = await (db.select(db.fieldTestEvents)..where((e) => e.sessionId.equals(sessionId))).get(); expect(events.isEmpty, isTrue); }); }); group('Field Test Logger - Export Service', () { test('Generates complete ZIP archive with README, JSON, CSVs, and diagnostics', () async { // 1. Create a simulated ride in DB final rideId = await db.into(db.rides).insert( RidesCompanion.insert( startTime: DateTime.now().subtract(const Duration(minutes: 20)), endTime: drift.Value(DateTime.now()), distanceMiles: const drift.Value(12.4), topSpeedMph: const drift.Value(78.5), averageSpeed: const drift.Value(45.2), maxLeanLeft: const drift.Value(34.2), maxLeanRight: const drift.Value(31.8), maxAccelG: const drift.Value(0.85), maxBrakingG: const drift.Value(-0.92), movingTime: const drift.Value(960), stoppedTime: const drift.Value(240), motorcycleMake: const drift.Value('Yamaha'), motorcycleModel: const drift.Value('MT-09'), ), ); // 2. Create telemetry samples for the ride await db.into(db.telemetrySamples).insert( TelemetrySamplesCompanion.insert( rideId: rideId, timestamp: DateTime.now(), latitude: const drift.Value(37.7749), longitude: const drift.Value(-122.4194), speed: const drift.Value(45.0), heading: const drift.Value(180.0), leanAngle: const drift.Value(-15.2), longitudinalG: const drift.Value(0.24), lateralG: const drift.Value(0.12), ), ); // 3. Start & log field test session final sessionId = await logger.startSession(rideId: rideId); expect(sessionId, isNotNull); logger.logGpsEvent( latitude: 37.7749, longitude: -122.4194, rawSpeed: 20.1, filteredSpeed: 20.1, gpsHealth: 'Healthy', accepted: true, ); logger.logImuDiagnostic( accelX: 0.1, accelY: 0.2, accelZ: 9.8, longitudinalG: 0.2, lateralG: 0.1, ); await logger.stopSession(); // 4. Test README / Report generation directly final session = await (db.select(db.fieldTestSessions)..where((s) => s.id.equals(sessionId!))).getSingle(); expect(session.completed, isTrue); final events = await (db.select(db.fieldTestEvents)..where((e) => e.sessionId.equals(sessionId!))).get(); expect(events.isNotEmpty, isTrue); expect(exportService, isNotNull); }); }); }