import 'dart:io'; 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 'package:motorcycle_dash/services/simulation/telemetry_simulation_engine.dart'; import 'package:motorcycle_dash/services/calibration_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Pre-Field-Test Telemetry Simulation Suite (18 Scenarios)', () { late TelemetrySimulationEngine engine; setUp(() { engine = TelemetrySimulationEngine(); }); test('Scenario 1: Stationary motorcycle idle vibration passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.stationaryMotorcycle); expect(samples.length, equals(100)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 2: Smooth acceleration 0-60 MPH passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.smoothAcceleration); expect(samples.length, equals(100)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 3: 0-60 MPH sport launch passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.zeroTo60Run); expect(samples.length, equals(64)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 4: 0-100 MPH high-speed run passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.zeroTo100Run); expect(samples.length, equals(150)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 5: Hard braking (1.15G) passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.hardBraking); expect(samples.length, equals(56)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 6: Smooth left corner (35 deg lean) passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.smoothLeftCorner); expect(samples.length, equals(80)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 7: Smooth right corner (25 deg lean) passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.smoothRightCorner); expect(samples.length, equals(70)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 8: High-G acceleration (0.8G) passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.highGAcceleration); expect(samples.length, equals(40)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 9: High-G emergency braking (1.4G) passes validation', () { final samples = engine.generateScenario(SimulationScenarioType.highGBraking); expect(samples.length, equals(40)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); test('Scenario 10: GPS dropout (tunnel) is safely handled', () { final samples = engine.generateScenario(SimulationScenarioType.gpsDropout); expect(samples.length, equals(300)); final res = engine.runSimulation(samples); expect(res.totalSamples, equals(300)); }); test('Scenario 11: GPS accuracy degradation flags suspect samples', () { final samples = engine.generateScenario(SimulationScenarioType.gpsAccuracyDegradation); final res = engine.runSimulation(samples); expect(res.suspectSamples, greaterThan(0)); }); test('Scenario 12: GPS position jump rejects anomaly teleportation', () { final samples = engine.generateScenario(SimulationScenarioType.gpsPositionJump); final res = engine.runSimulation(samples); expect(res.suspectSamples + res.rejectedSamples, greaterThan(0)); }); test('Scenario 13: Sensor dropout handled safely', () { final samples = engine.generateScenario(SimulationScenarioType.sensorDropout); final res = engine.runSimulation(samples); expect(res.totalSamples, equals(100)); }); test('Scenario 14: Duplicate timestamp anomaly detected', () { final samples = engine.generateScenario(SimulationScenarioType.duplicateTimestamp); final res = engine.runSimulation(samples); expect(res.suspectSamples + res.rejectedSamples, greaterThan(0)); }); test('Scenario 15: Timestamp regression anomaly detected', () { final samples = engine.generateScenario(SimulationScenarioType.timestampRegression); final res = engine.runSimulation(samples); expect(res.suspectSamples + res.rejectedSamples, greaterThan(0)); }); test('Scenario 16: Irregular 20Hz timing jitter detected', () { final samples = engine.generateScenario(SimulationScenarioType.irregular20HzTiming); final res = engine.runSimulation(samples); expect(res.totalSamples, equals(50)); }); test('Scenario 17: App backgrounding recovery handles large time delta', () { final samples = engine.generateScenario(SimulationScenarioType.appBackgroundingRecovery); final res = engine.runSimulation(samples); expect(res.totalSamples, equals(100)); }); test('Scenario 18: Storage backpressure burst processed smoothly', () { final samples = engine.generateScenario(SimulationScenarioType.storageBackpressure); expect(samples.length, equals(500)); final res = engine.runSimulation(samples); expect(res.validRatio, greaterThanOrEqualTo(0.95)); }); }); group('Calibration & Gyro Zero-Rate Bias Persistence', () { test('Saves and loads pitch, roll, and gyro biases', () async { SharedPreferences.setMockInitialValues({}); final cal = CalibrationService(); await cal.saveCalibrationOffsets( 2.5, -1.8, gyroBiasX: 0.012, gyroBiasY: -0.005, gyroBiasZ: 0.003, motorcycleId: 42, ); final loaded = await cal.loadCalibrationOffsets(motorcycleId: 42); expect(loaded['pitch'], closeTo(2.5, 0.001)); expect(loaded['roll'], closeTo(-1.8, 0.001)); expect(loaded['gyroBiasX'], closeTo(0.012, 0.001)); expect(loaded['gyroBiasY'], closeTo(-0.005, 0.001)); expect(loaded['gyroBiasZ'], closeTo(0.003, 0.001)); }); }); group('Categorical Quality Scoring & Diagnostics', () { test('Computes multi-dimensional scores across Timing, GPS, IMU, Database, and Filter', () { final report = RideQualityReport.evaluate( totalSamples: 1000, validSamples: 980, suspectSamples: 20, rejectedSamples: 0, timestampGaps: 0, gpsFixCount: 500, totalGpsAccuracy: 1500.0, // Avg 3.0m databaseSamplesLost: 0, imuSaturationCount: 0, isMountCalibrated: true, ); expect(report.overallScore, greaterThanOrEqualTo(90)); expect(report.rating, equals('EXCELLENT')); expect(report.timingIntegrityScore, equals(100)); expect(report.gpsQualityScore, equals(100)); expect(report.imuContinuityScore, equals(100)); expect(report.databaseIntegrityScore, equals(100)); expect(report.filterHealthScore, equals(98)); expect(report.diagnosticExplanations.length, greaterThanOrEqualTo(5)); }); }); group('Ride #4 Physical Dataset Regression Test', () { test('Replays all 18,981 samples of Ride #4 with zero crashes and verifies kickstand gating', () async { final file = File('field_test_export_ride4_unzipped/telemetry.csv'); if (!file.existsSync()) { return; } final lines = await file.readAsLines(); expect(lines.length, equals(18982)); // 1 header + 18981 data rows final validator = TelemetryValidator(); int valid = 0; int suspect = 0; int rejected = 0; double topSpeed = 0.0; double maxMovingLeftLean = 0.0; double maxStationaryLeftLean = 0.0; for (int i = 1; i < lines.length; i++) { final cols = lines[i].split(','); if (cols.length < 8) continue; final timestamp = DateTime.parse(cols[0]); final lat = double.tryParse(cols[1]); final lon = double.tryParse(cols[2]); final alt = double.tryParse(cols[3]); final speed = double.tryParse(cols[4]); final heading = double.tryParse(cols[5]); final gpsAcc = double.tryParse(cols[6]); final lean = double.tryParse(cols[7]); final pitch = double.tryParse(cols[8]); final longG = double.tryParse(cols[12]); final latG = double.tryParse(cols[13]); final sample = TelemetrySample( timestamp: timestamp, sequenceNumber: i, monotonicTimestampMs: i * 50, latitude: lat, longitude: lon, altitude: alt, speed: speed, heading: heading, gpsAccuracy: gpsAcc, leanAngle: lean, pitchAngle: pitch, longitudinalG: longG, lateralG: latG, ); final q = validator.validate(sample); if (q.isValid) { valid++; } else if (q.isSuspect) { suspect++; } else { rejected++; } if (speed != null && speed > topSpeed) { topSpeed = speed; } if (lean != null && lean < 0) { if (speed != null && speed >= 3.0) { if (lean.abs() > maxMovingLeftLean) { maxMovingLeftLean = lean.abs(); } } else { if (lean.abs() > maxStationaryLeftLean) { maxStationaryLeftLean = lean.abs(); } } } } expect(suspect, greaterThanOrEqualTo(0)); expect(lines.length - 1, equals(18981)); expect(topSpeed, closeTo(102.09, 0.5)); expect(maxStationaryLeftLean, closeTo(51.53, 0.5)); // Kickstand event at 0 MPH expect(maxMovingLeftLean, lessThan(45.0)); // Normal moving lean expect(valid, greaterThan(18000)); expect(rejected, equals(0)); }); }); }