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'; 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: Requirement 5 & 6 — Timing & Queue Observability Forensics', () { test('Detects and logs timestamp regressions when sample time reverses', () async { logger.startSession(101); final t1 = DateTime(2026, 8, 28, 12, 0, 10); final t2 = DateTime(2026, 8, 28, 12, 0, 5); // 5 seconds in the past! final s1 = TelemetrySample(timestamp: t1, speed: 45.0, gpsAccuracy: 3.0); final s2 = TelemetrySample(timestamp: t2, speed: 45.0, gpsAccuracy: 3.0); logger.processTelemetrySample(s1, validator.validate(s1)); logger.processTelemetrySample(s2, validator.validate(s2)); await logger.flush(); final events = logger.timelineEvents; expect(events.any((e) => e.eventType == FieldTestEventType.timestampError), isTrue); expect(events.firstWhere((e) => e.eventType == FieldTestEventType.timestampError).severity, EventSeverity.error); }); test('Queue bounds at capacity and drops INFO events first while retaining CRITICAL events', () async { logger.startSession(102); // Enqueue 999 INFO events for (int i = 0; i < 999; i++) { logger.log( eventType: FieldTestEventType.sampleAccepted, severity: EventSeverity.info, message: 'Info sample #$i', ); } expect(logger.currentQueueDepth, 1000); // 1 start event + 999 info events = 1000 (at capacity) // Add a critical event at full queue logger.log( eventType: FieldTestEventType.imuSaturation, severity: EventSeverity.critical, message: 'Critical IMU Saturation', ); // Queue length should remain capped at 1000 expect(logger.currentQueueDepth, 1000); expect(logger.droppedEventsCount, 1); expect(logger.oldestDroppedItem, isNotNull); await logger.flush(); expect(logger.totalFlushed, 1000); expect(logger.currentQueueDepth, 0); }); test('Tracks live peak queue depth, flush duration, and telemetry sample rate', () async { logger.startSession(103); final base = DateTime(2026, 8, 28, 14, 0, 0); for (int i = 0; i < 20; i++) { final sample = TelemetrySample( timestamp: base.add(Duration(milliseconds: i * 50)), speed: 35.0, gpsAccuracy: 2.5, ); logger.processTelemetrySample(sample, validator.validate(sample)); } expect(logger.peakQueueDepth, greaterThan(0)); expect(logger.queueCapacity, 1000); expect(logger.droppedEventsCount, 0); expect(logger.failedFlushCount, 0); }); }); group('Phase 11: Requirement 7 & 14 — Interrupted Crash Recovery & Summary Generation', () { test('Reconstructs complete forensic summary for unclosed interrupted ride', () async { final rideStart = DateTime(2026, 8, 28, 10, 0, 0); // Insert unclosed ride (endTime is null) final rideId = await db.into(db.rides).insert(RidesCompanion.insert( startTime: rideStart, )); // Simulate 50 realistic telemetry points for (int i = 0; i < 50; i++) { await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert( rideId: rideId, timestamp: rideStart.add(Duration(seconds: i)), latitude: drift.Value(37.7749 + (i * 0.0001)), longitude: const drift.Value(-122.4194), speed: const drift.Value(55.0), gpsAccuracy: const drift.Value(3.2), )); } // Generate crash-recovery forensic summary final summary = await logger.generateAndSaveSummary(rideId, wasRecovered: true); expect(summary.wasRecoveredAfterCrash, isTrue); expect(summary.wasInterrupted, isTrue); expect(summary.totalSamples, 50); expect(summary.validPercentage, 100.0); expect(summary.rideQualityScore, greaterThanOrEqualTo(85)); expect(summary.rideQualityRating, 'EXCELLENT'); expect(summary.exportIntegrity, 'VERIFIED_OK'); }); }); group('Phase 11: Requirement 15 — Forensic Export Package Content & Structure', () { test('Generates complete forensic session JSON and CSV with all required diagnostic headers', () async { final rideStart = DateTime(2026, 8, 28, 11, 0, 0); final rideId = await db.into(db.rides).insert(RidesCompanion.insert( startTime: rideStart, endTime: drift.Value(rideStart.add(const Duration(minutes: 30))), distanceMiles: const drift.Value(18.5), topSpeedMph: const drift.Value(78.2), )); logger.startSession(rideId); logger.log( eventType: FieldTestEventType.accelerationEvent, severity: EventSeverity.info, message: 'Hard launch 0.75g', speed: 45.0, longitudinalG: 0.75, ); await logger.flush(); await logger.generateAndSaveSummary(rideId); // Test CSV Export final csv = await exportService.generateFieldTestEventsCsv(rideId); expect(csv.startsWith('timestamp,ride_id,event_type,severity,message'), isTrue); expect(csv.contains('ACCELERATION_EVENT'), isTrue); // Test JSON Session Export final jsonStr = await exportService.generateFieldTestSessionJson(rideId); final Map parsed = jsonDecode(jsonStr); expect(parsed['exportVersion'], '1.0.0'); expect(parsed['rideMetadata']['id'], rideId); expect(parsed['summary']['totalSamples'], isNotNull); expect(parsed['events'], isNotEmpty); }); }); group('Phase 11: Requirement 16 — Deterministic Field-Test Replay Engine', () { test('Offline replay of synthetic ride yields identical deterministic validity classifications', () async { final base = DateTime(2026, 8, 28, 16, 0, 0); final samples = []; // 100 normal cruising samples for (int i = 0; i < 100; i++) { samples.add(TelemetrySample( timestamp: base.add(Duration(milliseconds: i * 50)), latitude: 37.7749 + (i * 0.00005), longitude: -122.4194 + (i * 0.00005), speed: 45.0, gpsAccuracy: 3.0, leanAngle: 12.0, longitudinalG: 0.05, lateralG: 0.12, )); } // Add 1 invalid GPS teleport jump sample samples.add(TelemetrySample( timestamp: base.add(const Duration(milliseconds: 100 * 50)), latitude: 38.5000, // Instant 80km jump! longitude: -122.4194, speed: 45.0, gpsAccuracy: 3.0, )); final result = await replayService.replaySamples(samples); expect(result.totalSamples, 101); expect(result.validSamples, 100); expect(result.rejectedSamples, 1); expect(result.validPercentage, closeTo(99.0, 0.1)); expect(result.rideQualityScore, greaterThanOrEqualTo(85)); expect(result.rideQualityRating, 'EXCELLENT'); expect(result.reconstructedEvents.any((e) => e.eventType == FieldTestEventType.sampleRejected), isTrue); }); }); }