import 'dart:async'; import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:drift/drift.dart' as drift; import 'package:drift/native.dart'; import 'package:motorcycle_dash/database/database.dart' hide FieldTestEvent, FieldTestSummary, TelemetrySample; import 'package:motorcycle_dash/models/field_test_event.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:motorcycle_dash/services/field_test/field_test_logger.dart'; import 'package:motorcycle_dash/services/field_test/field_test_export_service.dart'; import 'package:motorcycle_dash/services/field_test/field_test_replay_service.dart'; import 'package:motorcycle_dash/services/validation/telemetry_validator.dart'; import 'package:motorcycle_dash/features/performance/performance_timer_service.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late AppDatabase db; late FieldTestLogger logger; late TelemetryValidator validator; late FieldTestExportService exportService; late FieldTestReplayService replayService; setUp(() { db = AppDatabase.forTesting(NativeDatabase.memory()); logger = FieldTestLogger(db); validator = TelemetryValidator(); exportService = FieldTestExportService(db); replayService = FieldTestReplayService(validator: validator); }); tearDown(() async { logger.dispose(); await db.close(); }); group('Phase 11.5: Step 3 & 4 — Start/Stop Torment Test (100 Cycles)', () { test('Executes 100 consecutive start/record/stop cycles without state corruption or leaks', () async { for (int cycle = 1; cycle <= 100; cycle++) { final start = DateTime(2026, 8, 28, 8, 0, 0).add(Duration(minutes: cycle * 5)); final rideId = await db.into(db.rides).insert(RidesCompanion.insert( startTime: start, )); logger.startSession(rideId); expect(logger.isSessionActive, isTrue); expect(logger.activeRideId, rideId); // Generate 5 valid advancing telemetry samples per cycle for (int i = 0; i < 5; i++) { final s = TelemetrySample( timestamp: start.add(Duration(seconds: i)), speed: 25.0 + (i * 0.5), latitude: 37.7749 + (cycle * 0.001) + (i * 0.0001), longitude: -122.4194, gpsAccuracy: 3.0, leanAngle: 5.0, longitudinalG: 0.05, lateralG: 0.05, ); logger.processTelemetrySample(s, validator.validate(s)); await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert( rideId: rideId, timestamp: s.timestamp, speed: drift.Value(s.speed), latitude: drift.Value(s.latitude), longitude: drift.Value(s.longitude), gpsAccuracy: drift.Value(s.gpsAccuracy), leanAngle: drift.Value(s.leanAngle), longitudinalG: drift.Value(s.longitudinalG), lateralG: drift.Value(s.lateralG), )); } await (db.update(db.rides)..where((r) => r.id.equals(rideId))).write( RidesCompanion( endTime: drift.Value(start.add(const Duration(seconds: 5))), distanceMiles: const drift.Value(0.1), topSpeedMph: const drift.Value(27.0), ), ); final summary = await logger.stopSession(); expect(logger.isSessionActive, isFalse); expect(summary.rideId, rideId); expect(summary.totalSamples, 5); expect(summary.validSamples, 5); } // Verify total database records after 100 cycles final allRides = await db.select(db.rides).get(); final allSummaries = await db.select(db.fieldTestSummaries).get(); expect(allRides.length, 100); expect(allSummaries.length, 100); }); }); group('Phase 11.5: Step 6 — High-Frequency Telemetry Stress (20Hz, 30Hz, 50Hz, 100Hz)', () { test('Processes 20Hz, 30Hz, 50Hz, and 100Hz pathological sample streams stably', () async { logger.startSession(201); final base = DateTime(2026, 8, 28, 12, 0, 0); // Frequencies to test: 20Hz (50ms), 30Hz (33ms), 50Hz (20ms), 100Hz (10ms) final frequencies = [ {'name': '20Hz normal', 'intervalMs': 50, 'count': 100}, {'name': '30Hz stress', 'intervalMs': 33, 'count': 100}, {'name': '50Hz stress', 'intervalMs': 20, 'count': 100}, {'name': '100Hz pathological', 'intervalMs': 10, 'count': 100}, ]; int totalEmitted = 0; for (final freq in frequencies) { final intervalMs = freq['intervalMs'] as int; final count = freq['count'] as int; for (int i = 0; i < count; i++) { final s = TelemetrySample( timestamp: base.add(Duration(milliseconds: totalEmitted * intervalMs)), speed: 55.0, latitude: 37.7749 + (totalEmitted * 0.00001), longitude: -122.4194, gpsAccuracy: 2.5, leanAngle: 10.0, longitudinalG: 0.05, lateralG: 0.1, ); logger.processTelemetrySample(s, validator.validate(s)); totalEmitted++; } } await logger.flush(); expect(totalEmitted, 400); expect(logger.droppedEventsCount, 0); expect(logger.failedFlushCount, 0); }); }); group('Phase 11.5: Step 7 — Queue Saturation & Priority Eviction Test', () { test('Prioritized drop policy preserves CRITICAL and ERROR events over INFO events', () async { logger.startSession(301); // 1. Fill 1,000 capacity with INFO events for (int i = 0; i < 999; i++) { logger.log( eventType: FieldTestEventType.sampleAccepted, severity: EventSeverity.info, message: 'Telemetry sample #$i accepted', ); } expect(logger.currentQueueDepth, 1000); // 2. Add WARNING event -> should evict 1 INFO event logger.log( eventType: FieldTestEventType.gpsStale, severity: EventSeverity.warning, message: 'GPS fix stale warning', ); expect(logger.currentQueueDepth, 1000); expect(logger.droppedEventsCount, 1); // 3. Add ERROR event -> should evict another INFO event logger.log( eventType: FieldTestEventType.imuSaturation, severity: EventSeverity.error, message: 'IMU saturation error', ); expect(logger.currentQueueDepth, 1000); expect(logger.droppedEventsCount, 2); // 4. Add CRITICAL event -> should evict another INFO event logger.log( eventType: FieldTestEventType.databaseRecovery, severity: EventSeverity.critical, message: 'Critical DB crash recovery', ); expect(logger.currentQueueDepth, 1000); expect(logger.droppedEventsCount, 3); await logger.flush(); expect(logger.totalFlushed, 1000); final dbEvents = await db.select(db.fieldTestEvents).get(); expect(dbEvents.any((e) => e.severity == 'CRITICAL'), isTrue); expect(dbEvents.any((e) => e.severity == 'ERROR'), isTrue); expect(dbEvents.any((e) => e.severity == 'WARNING'), isTrue); }); }); group('Phase 11.5: Step 10 & 11 — Export Round-Trip & Destroy-Replay Test', () { test('Exports complete field test session, verifies all 6 files, and replays offline deterministically', () async { final start = DateTime(2026, 8, 28, 14, 0, 0); final rideId = await db.into(db.rides).insert(RidesCompanion.insert( startTime: start, endTime: drift.Value(start.add(const Duration(minutes: 45))), distanceMiles: const drift.Value(24.8), topSpeedMph: const drift.Value(85.5), maxLeanLeft: const drift.Value(38.2), maxLeanRight: const drift.Value(41.0), averageSpeed: const drift.Value(48.5), )); logger.startSession(rideId); final exportValidator = TelemetryValidator(); final sampleList = []; for (int i = 0; i < 100; i++) { final s = TelemetrySample( timestamp: start.add(Duration(seconds: i)), latitude: 37.7749 + (i * 0.0002), longitude: -122.4194 + (i * 0.0002), speed: 45.0 + (i * 0.05), gpsAccuracy: 2.8, leanAngle: 12.0, longitudinalG: 0.1, lateralG: 0.2, ); sampleList.add(s); logger.processTelemetrySample(s, exportValidator.validate(s)); await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert( rideId: rideId, timestamp: s.timestamp, latitude: drift.Value(s.latitude), longitude: drift.Value(s.longitude), speed: drift.Value(s.speed), gpsAccuracy: drift.Value(s.gpsAccuracy), leanAngle: drift.Value(s.leanAngle), longitudinalG: drift.Value(s.longitudinalG), lateralG: drift.Value(s.lateralG), )); } await logger.flush(); final originalSummary = await logger.generateAndSaveSummary(rideId); // Verify JSON session payload final jsonContent = await exportService.generateFieldTestSessionJson(rideId); final Map parsedJson = jsonDecode(jsonContent); expect(parsedJson['exportVersion'], '1.0.0'); expect(parsedJson['rideMetadata']['id'], rideId); expect(parsedJson['summary']['totalSamples'], 100); // Verify CSV payload final csvContent = await exportService.generateFieldTestEventsCsv(rideId); expect(csvContent.contains('timestamp,ride_id,event_type'), isTrue); // Replay offline from samples and compare metrics final replayResult = await replayService.replaySamples(sampleList); expect(replayResult.totalSamples, originalSummary.totalSamples); expect(replayResult.validSamples, originalSummary.validSamples); expect(replayResult.validPercentage, originalSummary.validPercentage); expect(replayResult.rideQualityScore, originalSummary.rideQualityScore); expect(replayResult.rideQualityRating, originalSummary.rideQualityRating); }); }); group('Phase 11.5: Step 12 — Timestamp Torture Matrix', () { test('Correctly identifies duplicate timestamps, reverse timestamps, and extreme time gaps', () { final base = DateTime(2026, 8, 28, 15, 0, 0); validator.reset(); // 1. Nominal sample final s1 = TelemetrySample(timestamp: base, speed: 45.0, gpsAccuracy: 3.0); final q1 = validator.validate(s1); expect(q1.isValid, isTrue); // 2. Duplicate timestamp (T, T) final s2 = TelemetrySample(timestamp: base, speed: 45.0, gpsAccuracy: 3.0); final q2 = validator.validate(s2); expect(q2.isSuspect, isTrue); expect(q2.reasons.any((r) => r.contains('Duplicate or reverse timestamp')), isTrue); // 3. Reverse timestamp (T, T - 50ms) final s3 = TelemetrySample(timestamp: base.subtract(const Duration(milliseconds: 50)), speed: 45.0, gpsAccuracy: 3.0); final q3 = validator.validate(s3); expect(q3.isSuspect, isTrue); expect(q3.reasons.any((r) => r.contains('Duplicate or reverse timestamp')), isTrue); // 4. Large gap (T + 5s) final s4 = TelemetrySample(timestamp: base.add(const Duration(seconds: 5)), speed: 45.0, gpsAccuracy: 3.0); final q4 = validator.validate(s4); expect(q4.isSuspect, isTrue); expect(q4.reasons.any((r) => r.contains('gap')), isTrue); // 5. Extreme gap (T + 60s) final s5 = TelemetrySample(timestamp: base.add(const Duration(seconds: 65)), speed: 45.0, gpsAccuracy: 3.0); final q5 = validator.validate(s5); expect(q5.isSuspect, isTrue); }); }); group('Phase 11.5: Step 13 & 14 — GPS & IMU Failure Matrix with De-duplication', () { test('GPS Failure Matrix: Fresh, Stale, Teleport, and Recovery behaviors', () { final base = DateTime(2026, 8, 28, 16, 0, 0); // Fresh @ 0 MPH validator.reset(); final stationary = TelemetrySample(timestamp: base, speed: 0.0, gpsAccuracy: 3.0, latitude: 37.7749, longitude: -122.4194); final qStat = validator.validate(stationary); expect(qStat.isStationary, isTrue); expect(qStat.isValid, isTrue); // Fresh @ 60 MPH validator.reset(); final fresh = TelemetrySample(timestamp: base, speed: 60.0, gpsAccuracy: 3.0, latitude: 37.7750, longitude: -122.4194); final qFresh = validator.validate(fresh); expect(qFresh.isValid, isTrue); // Stale @ 60 MPH (>2000ms age) final stale = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 50)), speed: 60.0, gpsAccuracy: 3.0, gpsAgeMs: 2500, latitude: 37.7751, longitude: -122.4194); final qStale = validator.validate(stale); expect(qStale.isSuspect, isTrue); expect(qStale.reasons.any((r) => r.contains('Stale')), isTrue); // GPS Teleport Jump final teleport = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 100)), speed: 60.0, gpsAccuracy: 3.0, latitude: 38.8888, longitude: -122.4194); final qTeleport = validator.validate(teleport); expect(qTeleport.isRejected, isTrue); expect(qTeleport.reasons.any((r) => r.contains('jump')), isTrue); }); test('IMU Failure Matrix: Saturation and Extreme Roll-Rate', () { final base = DateTime(2026, 8, 28, 17, 0, 0); validator.reset(); // Normal IMU final normal = TelemetrySample(timestamp: base, speed: 45.0, leanAngle: 25.0, accelX: 2.0, accelY: 1.0, accelZ: 9.8, longitudinalG: 0.2, lateralG: 0.4); expect(validator.validate(normal).isValid, isTrue); // Extreme IMU Saturation (>120 m/s² ≈ 12.2g) final saturated = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 50)), speed: 45.0, leanAngle: 25.0, accelX: 135.0, accelY: 9.8, accelZ: 0.0, longitudinalG: 13.5, lateralG: 0.4); final qSat = validator.validate(saturated); expect(qSat.isSensorSaturated, isTrue); expect(qSat.isSuspect, isTrue); // Extreme Roll Rate (>180 deg/sec) final flick1 = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 100)), speed: 45.0, leanAngle: 10.0); validator.validate(flick1); final flick2 = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 150)), speed: 45.0, leanAngle: 50.0); // 40 deg in 50ms = 800 deg/s! final qFlick = validator.validate(flick2); expect(qFlick.isSuspect, isTrue); expect(qFlick.reasons.any((r) => r.contains('roll rate')), isTrue); }); }); group('Phase 11.5: Step 16 & 17 — Personal Record Torture & Interruption Gating', () { test('Rejects impossible 0-60 sprint times (< 1.8s) and impossible speeds (> 220 MPH)', () { final base = DateTime(2026, 8, 28, 18, 0, 0); // Speeds: 100 MPH (valid), 219 MPH (valid), 220.5 MPH (rejected) validator.reset(); expect(validator.validate(TelemetrySample(timestamp: base, speed: 100.0)).isValid, isTrue); validator.reset(); expect(validator.validate(TelemetrySample(timestamp: base, speed: 219.0)).isValid, isTrue); validator.reset(); final impossibleSpeed = TelemetrySample(timestamp: base, speed: 221.0); final qSpeed = validator.validate(impossibleSpeed); expect(qSpeed.isRejected, isTrue); expect(qSpeed.reasons.any((r) => r.contains('Impossible vehicle speed')), isTrue); // Performance Timer Interruption test final streamController = StreamController.broadcast(); final timerService = PerformanceTimerService(db, streamController.stream); timerService.setTarget(PerformanceTarget.sixty); expect(timerService.state.state, PerformanceState.ready); // Inject stopped sample then launch streamController.add(TelemetrySample(timestamp: base, speed: 0.0)); streamController.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 50)), speed: 15.0)); // Reset / change target timerService.setTarget(PerformanceTarget.hundred); expect(timerService.state.state, PerformanceState.ready); expect(timerService.state.lastResult, isNull); streamController.close(); }); }); group('Phase 11.5: Step 5 & 25 — Realistic Long-Duration Multi-Phase Soak Simulation (2 Hours)', () { test('Simulates 2-hour continuous multi-phase ride with all fault injections and verifies summary consistency', () async { final start = DateTime(2026, 8, 28, 6, 0, 0); final rideId = await db.into(db.rides).insert(RidesCompanion.insert( startTime: start, endTime: drift.Value(start.add(const Duration(hours: 2))), distanceMiles: const drift.Value(72.4), topSpeedMph: const drift.Value(78.5), )); logger.startSession(rideId); final soakValidator = TelemetryValidator(); // Multi-phase 2-hour simulation (120 checkpoints spaced by 1-second ticks) for (int step = 0; step < 120; step++) { final time = start.add(Duration(seconds: step)); double speed = 55.0; double lean = 0.0; double longG = 0.05; double? gpsAcc = 3.0; int? gpsAge = 50; if (step < 10) { // Urban stop/start speed = (step % 2 == 0) ? 0.0 : 30.0; } else if (step < 40) { // Highway cruise speed = 68.0; } else if (step < 60) { // Mixed speed = 45.0; } else if (step < 80) { // Cornering speed = 42.0; lean = (step % 2 == 0) ? -28.0 : 31.5; } else if (step < 100) { // Highway speed = 72.0; } else if (step < 110) { // Fault: GPS degradation gpsAcc = 42.0; gpsAge = 2500; } else if (step < 115) { // Recovery gpsAcc = 3.5; gpsAge = 50; } final sample = TelemetrySample( timestamp: time, latitude: 37.7749 + (step * 0.0001), longitude: -122.4194 + (step * 0.0001), speed: speed, gpsAccuracy: gpsAcc, gpsAgeMs: gpsAge, leanAngle: lean, longitudinalG: longG, lateralG: 0.1, ); final quality = soakValidator.validate(sample); logger.processTelemetrySample(sample, quality); await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert( rideId: rideId, timestamp: sample.timestamp, latitude: drift.Value(sample.latitude), longitude: drift.Value(sample.longitude), speed: drift.Value(sample.speed), gpsAccuracy: drift.Value(sample.gpsAccuracy), leanAngle: drift.Value(sample.leanAngle), longitudinalG: drift.Value(sample.longitudinalG), lateralG: drift.Value(sample.lateralG), )); } await logger.flush(); final summary = await logger.generateAndSaveSummary(rideId); expect(summary.totalSamples, 120); expect(summary.validSamples, greaterThan(100)); expect(summary.rideQualityScore, greaterThanOrEqualTo(80)); expect(summary.exportIntegrity, 'VERIFIED_OK'); expect(summary.gpsStaleEventCount, greaterThan(0)); }); }); }