import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:motorcycle_dash/services/validation/telemetry_validator.dart'; import 'replay/telemetry_replay_engine.dart'; void main() { group('Phase 9 Forensic Validation: GPS & Sensor Freshness', () { late TelemetryValidator validator; setUp(() { validator = TelemetryValidator(); }); test('Fresh GPS position (<200ms) receives 1.0 confidence score', () { final sample = TelemetrySample( timestamp: DateTime(2026, 8, 28, 10, 0, 0), latitude: 37.7749, longitude: -122.4194, speed: 45.0, gpsAccuracy: 3.5, gpsAgeMs: 80, ); final quality = validator.validate(sample); expect(quality.isValid, isTrue); expect(quality.gpsConfidence, 1.0); expect(quality.overallConfidence, greaterThanOrEqualTo(0.90)); }); test('Stale GPS fix (>2000ms) while moving marks sample suspect with low confidence', () { final sample = TelemetrySample( timestamp: DateTime(2026, 8, 28, 10, 0, 0), latitude: 37.7749, longitude: -122.4194, speed: 55.0, gpsAccuracy: 5.0, gpsAgeMs: 2500, // 2.5s old fix ); final quality = validator.validate(sample); expect(quality.isSuspect, isTrue); expect(quality.gpsConfidence, lessThanOrEqualTo(0.20)); expect(quality.reasons.any((r) => r.contains('Stale GPS fix')), isTrue); }); test('GPS position teleportation (>110 m/s jump) is rejected with zero confidence', () { final baseTime = DateTime(2026, 8, 28, 10, 0, 0); // Sample 1: Baseline in San Francisco final s1 = TelemetrySample( timestamp: baseTime, latitude: 37.7749, longitude: -122.4194, speed: 30.0, gpsAccuracy: 3.0, gpsAgeMs: 50, ); validator.validate(s1); // Sample 2: Sudden 500m coordinate jump in 50ms (teleportation) final s2 = TelemetrySample( timestamp: baseTime.add(const Duration(milliseconds: 50)), latitude: 37.7800, // ~570 meters away longitude: -122.4194, speed: 30.0, gpsAccuracy: 3.0, gpsAgeMs: 50, ); final quality2 = validator.validate(s2); expect(quality2.isRejected, isTrue); expect(quality2.gpsConfidence, 0.0); expect(quality2.reasons.any((r) => r.contains('GPS position jump detected')), isTrue); }); test('RideQualityReport evaluates excellent 100-point session correctly', () { final report = RideQualityReport.evaluate( totalSamples: 1000, validSamples: 980, suspectSamples: 18, rejectedSamples: 2, timestampGaps: 1, gpsFixCount: 500, totalGpsAccuracy: 1750.0, // 3.5m average isMountCalibrated: true, ); expect(report.overallScore, greaterThanOrEqualTo(90)); expect(report.rating, 'EXCELLENT'); expect(report.isMountCalibrated, isTrue); }); test('RideQualityReport evaluates degraded session with poor GPS and uncalibrated mount', () { final report = RideQualityReport.evaluate( totalSamples: 1000, validSamples: 450, suspectSamples: 400, rejectedSamples: 150, timestampGaps: 12, gpsFixCount: 100, totalGpsAccuracy: 4500.0, // 45m average isMountCalibrated: false, ); expect(report.overallScore, lessThan(60)); expect(report.rating, anyOf('POOR', 'INVALID')); }); }); group('Phase 9 Forensic Validation: Replay Engine & Scenario Playback', () { late TelemetryReplayEngine engine; setUp(() { engine = TelemetryReplayEngine(); }); tearDown(() { engine.dispose(); }); test('Replays straight-line cruise scenario with 100% validity', () async { final baseTime = DateTime(2026, 8, 28, 12, 0, 0); final cruiseSamples = List.generate(40, (i) { return TelemetrySample( timestamp: baseTime.add(Duration(milliseconds: i * 50)), latitude: 37.7749 + (i * 0.0001), longitude: -122.4194, speed: 60.0, heading: 0.0, gpsAccuracy: 2.8, gpsAgeMs: 50, leanAngle: 0.0, longitudinalG: 0.0, lateralG: 0.0, ); }); final scenario = ReplayScenario(name: 'Straight Cruise', samples: cruiseSamples); final results = await engine.play(scenario); expect(results.length, 40); expect(results.every((s) => s.quality!.isValid), isTrue); expect(engine.validator.rejectedCount, 0); }); test('Replays aggressive cornering scenario with valid lean tracking', () async { final baseTime = DateTime(2026, 8, 28, 12, 0, 0); final cornerSamples = List.generate(30, (i) { final lean = (i < 15) ? (i * 3.0) : (45.0 - (i - 15) * 3.0); // 0 -> 45° -> 0° return TelemetrySample( timestamp: baseTime.add(Duration(milliseconds: i * 50)), latitude: 37.7749, longitude: -122.4194 + (i * 0.0001), speed: 42.0, heading: 90.0, gpsAccuracy: 3.2, gpsAgeMs: 50, leanAngle: lean, longitudinalG: -0.05, lateralG: lean / 45.0 * 0.85, ); }); final scenario = ReplayScenario(name: 'Right Sweeper Corner', samples: cornerSamples); final results = await engine.play(scenario); expect(results.length, 30); expect(results.every((s) => s.quality!.isValid), isTrue); expect(results[15].leanAngle, 45.0); }); test('Replays GPS dropout scenario and flags stale/gap samples correctly', () async { final baseTime = DateTime(2026, 8, 28, 12, 0, 0); final samples = [ // Good sample TelemetrySample( timestamp: baseTime, latitude: 37.7749, longitude: -122.4194, speed: 55.0, gpsAccuracy: 3.0, gpsAgeMs: 50, ), // Stale GPS sample (>2s old) TelemetrySample( timestamp: baseTime.add(const Duration(milliseconds: 50)), latitude: 37.7749, longitude: -122.4194, speed: 55.0, gpsAccuracy: 3.0, gpsAgeMs: 2200, ), // Teleportation sample TelemetrySample( timestamp: baseTime.add(const Duration(milliseconds: 100)), latitude: 37.8500, // 8km jump longitude: -122.4194, speed: 55.0, gpsAccuracy: 3.0, gpsAgeMs: 50, ), ]; final scenario = ReplayScenario(name: 'GPS Dropout & Jump', samples: samples); final results = await engine.play(scenario); expect(results[0].quality!.isValid, isTrue); expect(results[1].quality!.isSuspect, isTrue); expect(results[2].quality!.isRejected, isTrue); }); }); }