import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_compliance.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_service.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() async { SharedPreferences.setMockInitialValues({}); }); group('RoadAlertCompliance Jurisdiction Evaluation Tests', () { test('correctly classifies United States locations as standard', () { // Los Angeles, CA expect( RoadAlertCompliance.evaluateJurisdiction(34.0522, -118.2437), equals(AlertComplianceJurisdiction.standard), ); // New York, NY expect( RoadAlertCompliance.evaluateJurisdiction(40.7128, -74.0060), equals(AlertComplianceJurisdiction.standard), ); // Deals Gap, NC / TN (Tail of the Dragon) expect( RoadAlertCompliance.evaluateJurisdiction(35.4712, -83.9234), equals(AlertComplianceJurisdiction.standard), ); }); test('correctly classifies Germany and Switzerland as prohibited', () { // Berlin, Germany expect( RoadAlertCompliance.evaluateJurisdiction(52.5200, 13.4050), equals(AlertComplianceJurisdiction.prohibited), ); // Munich, Germany expect( RoadAlertCompliance.evaluateJurisdiction(48.1351, 11.5820), equals(AlertComplianceJurisdiction.prohibited), ); // Zurich, Switzerland expect( RoadAlertCompliance.evaluateJurisdiction(47.3769, 8.5417), equals(AlertComplianceJurisdiction.prohibited), ); // Geneva, Switzerland expect( RoadAlertCompliance.evaluateJurisdiction(46.2044, 6.1432), equals(AlertComplianceJurisdiction.prohibited), ); }); test('correctly classifies France as frenchDangerZone', () { // Paris, France expect( RoadAlertCompliance.evaluateJurisdiction(48.8566, 2.3522), equals(AlertComplianceJurisdiction.frenchDangerZone), ); // Lyon, France expect( RoadAlertCompliance.evaluateJurisdiction(45.7640, 4.8357), equals(AlertComplianceJurisdiction.frenchDangerZone), ); }); }); group('RoadAlertCompliance Alert Transformation Tests', () { final now = DateTime.now(); final List testAlerts = [ RoadAlert( id: 'cam_1', providerId: 'here_safety_cameras', category: RoadAlertCategory.speedCamera, latitude: 48.8566, longitude: 2.3522, description: 'Fixed speed camera 50 km/h', reportedAt: now, lastUpdated: now, source: 'HERE Technologies', ), RoadAlert( id: 'police_trap_1', providerId: 'here_safety_cameras', category: RoadAlertCategory.police, latitude: 52.5200, longitude: 13.4050, description: 'Mobile Radar Speed Trap', reportedAt: now, lastUpdated: now, source: 'HERE Technologies', ), RoadAlert( id: 'hazard_1', providerId: 'swarm_alerts', category: RoadAlertCategory.hazard, latitude: 52.5200, longitude: 13.4050, description: 'Gravel on road', reportedAt: now, lastUpdated: now, source: 'MotorSignal Rider Swarm', ), RoadAlert( id: 'accident_1', providerId: 'tomtom', category: RoadAlertCategory.accident, latitude: 48.8566, longitude: 2.3522, description: 'Multi-vehicle collision', reportedAt: now, lastUpdated: now, source: 'TomTom', ), ]; test('Germany / Switzerland: speed camera and radar trap suppressed; hazards preserved', () { final filtered = RoadAlertCompliance.applyCompliance( testAlerts, userLat: 52.5200, userLng: 13.4050, complianceEnabled: true, ); // Speed camera and radar trap must be filtered out expect(filtered.any((a) => a.id == 'cam_1'), isFalse); expect(filtered.any((a) => a.id == 'police_trap_1'), isFalse); // Hazards and accidents must remain expect(filtered.any((a) => a.id == 'hazard_1'), isTrue); expect(filtered.any((a) => a.id == 'accident_1'), isTrue); }); test('France: speed camera converted into Danger Zone hazard advisory', () { final filtered = RoadAlertCompliance.applyCompliance( testAlerts, userLat: 48.8566, userLng: 2.3522, complianceEnabled: true, ); final camAlert = filtered.firstWhere((a) => a.id == 'cam_1'); expect(camAlert.category, equals(RoadAlertCategory.hazard)); expect(camAlert.description, contains('Zone de Danger')); }); test('United States: full exact alerts preserved with 100% fidelity', () { final usAlerts = RoadAlertCompliance.applyCompliance( testAlerts, userLat: 34.0522, userLng: -118.2437, complianceEnabled: true, ); expect(usAlerts.length, equals(testAlerts.length)); expect(usAlerts.firstWhere((a) => a.id == 'cam_1').category, equals(RoadAlertCategory.speedCamera)); expect(usAlerts.firstWhere((a) => a.id == 'police_trap_1').category, equals(RoadAlertCategory.police)); }); test('When compliance is disabled, alerts pass through unmodified', () { final passThrough = RoadAlertCompliance.applyCompliance( testAlerts, userLat: 52.5200, // Germany userLng: 13.4050, complianceEnabled: false, ); expect(passThrough.length, equals(testAlerts.length)); expect(passThrough.any((a) => a.id == 'cam_1'), isTrue); }); }); group('Phase 4 Riverpod State Management Tests', () { test('roadAlertAudioModeProvider defaults to voice and switches to tacticalTone', () async { final container = ProviderContainer(); addTearDown(container.dispose); expect(container.read(roadAlertAudioModeProvider), equals(RoadAlertAudioMode.voice)); await container .read(roadAlertAudioModeProvider.notifier) .setMode(RoadAlertAudioMode.tacticalTone); expect(container.read(roadAlertAudioModeProvider), equals(RoadAlertAudioMode.tacticalTone)); }); test('leanInterlockEnabledProvider defaults to true and toggles properly', () async { final container = ProviderContainer(); addTearDown(container.dispose); expect(container.read(leanInterlockEnabledProvider), isTrue); await container.read(leanInterlockEnabledProvider.notifier).setEnabled(false); expect(container.read(leanInterlockEnabledProvider), isFalse); }); test('jurisdictionComplianceEnabledProvider defaults to true and toggles properly', () async { final container = ProviderContainer(); addTearDown(container.dispose); expect(container.read(jurisdictionComplianceEnabledProvider), isTrue); await container.read(jurisdictionComplianceEnabledProvider.notifier).setEnabled(false); expect(container.read(jurisdictionComplianceEnabledProvider), isFalse); }); }); group('Lean-Angle Cornering Safety Interlock Tests', () { test('RoadAlertService tracks lean angle and cornering state correctly', () async { final leanController = StreamController.broadcast(); addTearDown(leanController.close); final container = ProviderContainer( overrides: [ leanStreamOverrideProvider.overrideWithValue(leanController.stream), ], ); addTearDown(container.dispose); final service = container.read(roadAlertServiceProvider); // Initial lean is 0 (upright) expect(service.currentLeanAngle, equals(0.0)); expect(service.isCornering, isFalse); // Lean into right turn: 22 degrees leanController.add(22.0); await Future.delayed(const Duration(milliseconds: 50)); expect(service.currentLeanAngle, equals(22.0)); expect(service.isCornering, isTrue); // Return to upright: 3 degrees leanController.add(3.0); await Future.delayed(const Duration(milliseconds: 50)); // Just returned, but under 1.5s recovery gate: still marked cornering expect(service.currentLeanAngle, equals(3.0)); expect(service.isCornering, isTrue); // Wait 1.6 seconds for upright recovery await Future.delayed(const Duration(milliseconds: 1600)); expect(service.isCornering, isFalse); }); test('Alert audio is deferred when bike is cornering at 25 degrees', () async { final leanController = StreamController.broadcast(); addTearDown(leanController.close); final container = ProviderContainer( overrides: [ leanStreamOverrideProvider.overrideWithValue(leanController.stream), ], ); addTearDown(container.dispose); final service = container.read(roadAlertServiceProvider); // Set bike cornering leanController.add(25.0); await Future.delayed(const Duration(milliseconds: 50)); expect(service.isCornering, isTrue); final now = DateTime.now(); // Inject alert 200m ahead final alert = RoadAlert( id: 'apex_trap_1', providerId: 'here_safety_cameras', category: RoadAlertCategory.police, latitude: 34.0522, longitude: -118.2437, description: 'Speed enforcement trap', reportedAt: now, lastUpdated: now, distanceFromUser: 200.0, source: 'HERE Technologies', ); service.simulateAlert(alert); await Future.delayed(const Duration(milliseconds: 50)); // Alert should be in cached alerts expect(service.alertsStream, emitsThrough(predicate>((l) => l.isNotEmpty))); }); }); }