import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart'; import 'package:motorcycle_dash/services/alerts/swarm_road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/swarm_road_alert_provider.dart'; import 'package:motorcycle_dash/services/alerts/swarm_road_alert_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({}); }); group('SwarmReport Model Tests', () { test('roundtrip serialization to/from JSON', () { final now = DateTime.now(); final expires = now.add(const Duration(hours: 1)); final report = SwarmReport( id: 'swarm_test_1', category: RoadAlertCategory.police, latitude: 35.3934, longitude: -83.9261, heading: 180.0, roadName: 'US-129 Deals Gap', description: 'Tennessee Highway Patrol stationary radar', reporterUid: 'rider_abc', reportedAt: now, expiresAt: expires, confirmations: 3, dismissals: 1, groupRideId: 'group_ride_123', ); final json = report.toJson(); expect(json['id'], 'swarm_test_1'); expect(json['category'], 'police'); expect(json['latitude'], 35.3934); expect(json['longitude'], -83.9261); expect(json['confirmations'], 3); expect(json['dismissals'], 1); expect(json['groupRideId'], 'group_ride_123'); final fromJson = SwarmReport.fromJson(json); expect(fromJson.id, report.id); expect(fromJson.category, RoadAlertCategory.police); expect(fromJson.latitude, 35.3934); expect(fromJson.longitude, -83.9261); expect(fromJson.confirmations, 3); expect(fromJson.dismissals, 1); expect(fromJson.groupRideId, 'group_ride_123'); }); test('confidence scoring based on confirmations and dismissals', () { final now = DateTime.now(); final expires = now.add(const Duration(hours: 1)); // 3 confirmations, 1 dismissal -> 3/4 = 0.75 final r1 = SwarmReport( id: '1', category: RoadAlertCategory.police, latitude: 35.0, longitude: -83.0, description: 'Test', reporterUid: 'u1', reportedAt: now, expiresAt: expires, confirmations: 3, dismissals: 1, ); expect(r1.confidence, closeTo(0.75, 0.001)); // 1 confirmation, 0 dismissals -> 1/1 = 1.0 final r2 = SwarmReport( id: '2', category: RoadAlertCategory.hazard, latitude: 35.0, longitude: -83.0, description: 'Test', reporterUid: 'u1', reportedAt: now, expiresAt: expires, confirmations: 1, dismissals: 0, ); expect(r2.confidence, 1.0); // Clamp minimum final r3 = SwarmReport( id: '3', category: RoadAlertCategory.hazard, latitude: 35.0, longitude: -83.0, description: 'Test', reporterUid: 'u1', reportedAt: now, expiresAt: expires, confirmations: 0, dismissals: 10, ); expect(r3.confidence, 0.1); }); test('toRoadAlert maps severity and source attributes properly', () { final now = DateTime.now(); final expires = now.add(const Duration(hours: 1)); final policeReport = SwarmReport( id: 'police_1', category: RoadAlertCategory.police, latitude: 35.39, longitude: -83.92, description: 'Speed trap', reporterUid: 'uid1', reportedAt: now, expiresAt: expires, ); final policeAlert = policeReport.toRoadAlert(distanceFromUser: 250.0); expect(policeAlert.providerId, 'swarm_alerts'); expect(policeAlert.severity, RoadAlertSeverity.high); expect(policeAlert.source, 'MotorSignal Swarm'); expect(policeAlert.sourceType, RoadAlertSourceType.userReported); expect(policeAlert.distanceFromUser, 250.0); final packReport = SwarmReport( id: 'hazard_1', category: RoadAlertCategory.hazard, latitude: 35.39, longitude: -83.92, description: 'Gravel in corner', reporterUid: 'uid2', reportedAt: now, expiresAt: expires, groupRideId: 'pack_77', ); final packAlert = packReport.toRoadAlert(); expect(packAlert.severity, RoadAlertSeverity.medium); expect(packAlert.source, 'Pack Rider Report'); }); test('isExpired detects when TTL has lapsed', () { final past = DateTime.now().subtract(const Duration(minutes: 5)); final report = SwarmReport( id: 'expired_1', category: RoadAlertCategory.police, latitude: 35.0, longitude: -83.0, description: 'Old trap', reporterUid: 'u1', reportedAt: past.subtract(const Duration(hours: 1)), expiresAt: past, ); expect(report.isExpired, isTrue); }); }); group('SwarmRoadAlertService Cloud & Cache Tests', () { late FakeFirebaseFirestore fakeFirestore; late SwarmRoadAlertService service; setUp(() { fakeFirestore = FakeFirebaseFirestore(); service = SwarmRoadAlertService(firestore: fakeFirestore); }); test('reportAlert saves to Cloud Firestore and caches in memory', () async { final report = await service.reportAlert( category: RoadAlertCategory.police, latitude: 34.0522, longitude: -118.2437, roadName: 'Mulholland Drive', description: 'Radar trap near lookout', ); expect(report.id, startsWith('swarm_')); expect(report.category, RoadAlertCategory.police); expect(report.confirmations, 1); expect(report.dismissals, 0); // Check Firestore doc final doc = await fakeFirestore.collection('road_alerts').doc(report.id).get(); expect(doc.exists, isTrue); expect(doc.data()!['category'], 'police'); expect(doc.data()!['latitude'], 34.0522); expect(doc.data()!['confirmations'], 1); // Default TTL for police is 60 minutes final reportedAt = DateTime.fromMillisecondsSinceEpoch(doc.data()!['reportedAt'] as int); final expiresAt = DateTime.fromMillisecondsSinceEpoch(doc.data()!['expiresAt'] as int); expect(expiresAt.difference(reportedAt).inMinutes, 60); }); test('reportAlert hazard defaults to 120 minutes TTL', () async { final report = await service.reportAlert( category: RoadAlertCategory.hazard, latitude: 35.0, longitude: -83.0, description: 'Gravel patch', ); final doc = await fakeFirestore.collection('road_alerts').doc(report.id).get(); final reportedAt = DateTime.fromMillisecondsSinceEpoch(doc.data()!['reportedAt'] as int); final expiresAt = DateTime.fromMillisecondsSinceEpoch(doc.data()!['expiresAt'] as int); expect(expiresAt.difference(reportedAt).inMinutes, 120); }); test('confirmAlert increments confirmations in memory and Firestore', () async { final report = await service.reportAlert( category: RoadAlertCategory.police, latitude: 34.0, longitude: -118.0, ); await service.confirmAlert(report.id); final doc = await fakeFirestore.collection('road_alerts').doc(report.id).get(); expect(doc.data()!['confirmations'], 2); }); test('clearAlert increments dismissals and auto-expires after 2 dismissals', () async { final report = await service.reportAlert( category: RoadAlertCategory.hazard, latitude: 34.0, longitude: -118.0, ); // 1st clear: increments dismissals to 1 await service.clearAlert(report.id); var doc = await fakeFirestore.collection('road_alerts').doc(report.id).get(); expect(doc.data()!['dismissals'], 1); // 2nd clear: dismissals becomes 2, which is >= confirmations (1) -> expires immediately await service.clearAlert(report.id); doc = await fakeFirestore.collection('road_alerts').doc(report.id).get(); expect(doc.data()!['dismissals'], 2); final expiresAt = doc.data()!['expiresAt'] as int; expect(DateTime.now().millisecondsSinceEpoch >= expiresAt - 1000, isTrue); }); test('getAlertsNear finds alerts within radius and excludes distant ones', () async { // 1. Report alert near Downtown LA (lat: 34.05, lng: -118.25) await service.reportAlert( category: RoadAlertCategory.police, latitude: 34.0500, longitude: -118.2500, description: 'Downtown trap', ); // 2. Report alert in San Francisco (distant: > 500 km) await service.reportAlert( category: RoadAlertCategory.hazard, latitude: 37.7749, longitude: -122.4194, description: 'SF hazard', ); // Query around Downtown LA with 2000 meter radius final nearbyAlerts = await service.getAlertsNear( latitude: 34.0505, longitude: -118.2505, radiusMeters: 2000, ); expect(nearbyAlerts.length, 1); expect(nearbyAlerts.first.category, RoadAlertCategory.police); expect(nearbyAlerts.first.description, 'Downtown trap'); expect(nearbyAlerts.first.distanceFromUser, isNotNull); expect(nearbyAlerts.first.distanceFromUser!, lessThan(2000)); }); test('offline fallback works seamlessly without Cloud Firestore', () async { final offlineService = SwarmRoadAlertService(firestore: null, auth: null); expect(offlineService.isCloudAvailable, isFalse); final report = await offlineService.reportAlert( category: RoadAlertCategory.accident, latitude: 35.39, longitude: -83.92, description: 'Offline crash report', ); expect(report.id, startsWith('swarm_')); final alerts = await offlineService.getAlertsNear( latitude: 35.39, longitude: -83.92, radiusMeters: 1000, ); expect(alerts.length, 1); expect(alerts.first.category, RoadAlertCategory.accident); }); }); group('SwarmRoadAlertProvider Tests', () { test('implements RoadAlertProvider contract correctly', () async { final service = SwarmRoadAlertService(firestore: null, auth: null); final provider = SwarmRoadAlertProvider(service); expect(provider.id, 'swarm_alerts'); expect(provider.name, 'MotorSignal Rider Swarm'); expect(provider.supportedCategories.contains(RoadAlertCategory.police), isTrue); expect(provider.supportedCategories.contains(RoadAlertCategory.hazard), isTrue); final status = await provider.testConnection(''); expect(status, RoadAlertProviderStatus.connected); }); }); group('Riverpod Swarm Provider Wiring Tests', () { test('activeRoadAlertProviders respects swarmAlertsEnabledProvider toggle', () async { final container = ProviderContainer(); addTearDown(container.dispose); // Default is enabled final initialEnabled = container.read(swarmAlertsEnabledProvider); expect(initialEnabled, isTrue); var activeProviders = container.read(activeRoadAlertProviders); expect(activeProviders.any((p) => p.id == 'swarm_alerts'), isTrue); // Disable swarm alerts await container.read(swarmAlertsEnabledProvider.notifier).setEnabled(false); expect(container.read(swarmAlertsEnabledProvider), isFalse); activeProviders = container.read(activeRoadAlertProviders); expect(activeProviders.any((p) => p.id == 'swarm_alerts'), isFalse); // Re-enable await container.read(swarmAlertsEnabledProvider.notifier).setEnabled(true); expect(container.read(swarmAlertsEnabledProvider), isTrue); activeProviders = container.read(activeRoadAlertProviders); expect(activeProviders.any((p) => p.id == 'swarm_alerts'), isTrue); }); }); }