import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/waze_osint_road_alert_provider.dart'; void main() { group('WazeOsintRoadAlertProvider Category Mapping', () { test('maps all Waze types and subtypes to 10 native MotorSignal categories', () { expect( WazeOsintRoadAlertProvider.mapTypeToCategory('POLICE', null), RoadAlertCategory.police, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('POLICE', 'POLICE_HIDDEN'), RoadAlertCategory.police, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('ACCIDENT', 'ACCIDENT_MAJOR'), RoadAlertCategory.accident, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('JAM', 'JAM_HEAVY_TRAFFIC'), RoadAlertCategory.traffic, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('ROAD_CLOSED', 'ROAD_CLOSED_HAZARD'), RoadAlertCategory.roadClosure, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('WEATHERHAZARD', 'HAZARD_WEATHER_ICE'), RoadAlertCategory.weatherHazard, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_ON_ROAD_POT_HOLE'), RoadAlertCategory.pothole, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_ON_ROAD_OBJECT'), RoadAlertCategory.debris, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_ON_SHOULDER_CAR_STOPPED'), RoadAlertCategory.disabledVehicle, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_ON_ROAD_CONSTRUCTION'), RoadAlertCategory.construction, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_GENERIC'), RoadAlertCategory.hazard, ); expect( WazeOsintRoadAlertProvider.mapTypeToCategory('OTHER', null), RoadAlertCategory.other, ); }); }); group('WazeOsintRoadAlertProvider JSON Fixture Parsing', () { late WazeOsintRoadAlertProvider provider; setUp(() { provider = WazeOsintRoadAlertProvider(); }); test('parses realistic LiveMap GeoRSS JSON fixture with multiple alerts', () { final fixture = { 'alerts': [ { 'uuid': 'alert_police_001', 'type': 'POLICE', 'subtype': 'POLICE_HIDDEN', 'location': {'x': -83.9261, 'y': 35.3934}, 'street': 'US-129 Deals Gap', 'city': 'Robbinsville', 'magvar': 180, 'reportDescription': 'Trooper on bike hiding behind curve', 'pubMillis': DateTime.now().millisecondsSinceEpoch, 'confidence': 5, 'reliability': 8, 'nThumbsUp': 4, }, { 'uuid': 'alert_pothole_002', 'type': 'HAZARD', 'subtype': 'HAZARD_ON_ROAD_POT_HOLE', 'latitude': 35.4000, 'longitude': -83.9300, 'street': 'Cherohala Skyway', 'reportDescription': 'Deep rim-bending hole mid-corner', 'pubMillis': DateTime.now().millisecondsSinceEpoch, 'nThumbsUp': 2, }, { 'uuid': 'alert_debris_003', 'type': 'HAZARD', 'subtype': 'HAZARD_ON_ROAD_OBJECT', 'location': {'x': -83.9200, 'y': 35.3900}, 'street': 'NC-28 Moonshiner', 'nThumbsUp': 0, }, { 'uuid': 'alert_invalid_coords', 'type': 'POLICE', 'latitude': 999.0, // invalid latitude 'longitude': -83.90, }, { 'uuid': 'alert_missing_type', 'location': {'x': -83.90, 'y': 35.0}, } ] }; final parsed = provider.parseWazeResponse(fixture); expect(parsed.length, 3); final police = parsed.firstWhere((a) => a.id == 'waze_osint_alert_police_001'); expect(police.category, RoadAlertCategory.police); expect(police.latitude, 35.3934); expect(police.longitude, -83.9261); expect(police.roadName, 'US-129 Deals Gap, Robbinsville'); expect(police.description, 'Trooper on bike hiding behind curve'); expect(police.severity, RoadAlertSeverity.high); expect(police.confirmations, 5); // 1 base + 4 thumbsUp expect(police.isCommunityConfirmed, isTrue); final pothole = parsed.firstWhere((a) => a.id == 'waze_osint_alert_pothole_002'); expect(pothole.category, RoadAlertCategory.pothole); expect(pothole.latitude, 35.4000); expect(pothole.longitude, -83.9300); expect(pothole.roadName, 'Cherohala Skyway'); expect(pothole.confirmations, 3); // 1 base + 2 thumbsUp expect(pothole.isCommunityConfirmed, isTrue); final debris = parsed.firstWhere((a) => a.id == 'waze_osint_alert_debris_003'); expect(debris.category, RoadAlertCategory.debris); expect(debris.latitude, 35.3900); expect(debris.longitude, -83.9200); expect(debris.confirmations, 1); expect(debris.isCommunityConfirmed, isFalse); }); test('deduplicates alerts sharing the same uuid', () { final duplicateFixture = { 'alerts': [ { 'uuid': 'dup_1', 'type': 'ACCIDENT', 'location': {'x': -84.0, 'y': 35.0}, }, { 'uuid': 'dup_1', 'type': 'ACCIDENT', 'location': {'x': -84.0, 'y': 35.0}, }, ] }; final parsed = provider.parseWazeResponse(duplicateFixture); expect(parsed.length, 1); }); }); group('WazeOsintRoadAlertProvider HTTP Client & Rate Limiting', () { test('getAlerts queries bounding box and caches results', () async { int requestCount = 0; final mockClient = MockClient((request) async { requestCount++; expect(request.url.host, 'www.waze.com'); expect(request.url.path, '/live-map/api/georss'); expect(request.url.queryParameters['types'], 'alerts'); expect(request.headers['User-Agent'], 'MotorSignal-Community-Alerts/1.0'); final payload = { 'alerts': [ { 'uuid': 'test_waze_live_1', 'type': 'POLICE', 'location': {'x': -83.9261, 'y': 35.3934}, 'street': 'US-129 Deals Gap', 'pubMillis': DateTime.now().millisecondsSinceEpoch, 'nThumbsUp': 3, } ] }; return http.Response(json.encode(payload), 200, headers: { 'content-type': 'application/json', }); }); final provider = WazeOsintRoadAlertProvider( client: mockClient, minQueryInterval: const Duration(seconds: 30), ); // 1. Initial query executes HTTP request final alerts1 = await provider.getAlerts( latitude: 35.3934, longitude: -83.9261, radiusMeters: 5000, ); expect(requestCount, 1); expect(alerts1.length, 1); expect(alerts1.first.category, RoadAlertCategory.police); // 2. Immediate second query within 30s interval uses cache without extra HTTP call final alerts2 = await provider.getAlerts( latitude: 35.3934, longitude: -83.9261, radiusMeters: 5000, ); expect(requestCount, 1); // No new network call expect(alerts2.length, 1); }); test('testConnection with injected client returns connected', () async { final mockClient = MockClient((request) async { return http.Response(json.encode({'alerts': []}), 200); }); final provider = WazeOsintRoadAlertProvider(client: mockClient); final status = await provider.testConnection(''); expect(status, RoadAlertProviderStatus.connected); }); }); group('WazeOsintRoadAlertProvider Environment / Region Selection', () { test('resolves North America (na) for US, Canada, Mexico, Puerto Rico, Alaska, Hawaii', () { // Deals Gap, NC (US-129) expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 35.3934, longitude: -83.9261), 'na', ); // Los Angeles, CA expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 34.0522, longitude: -118.2437), 'na', ); // Toronto, ON, Canada expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 43.6532, longitude: -79.3832), 'na', ); // Mexico City, Mexico expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 19.4326, longitude: -99.1332), 'na', ); // Honolulu, HI expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 21.3069, longitude: -157.8583), 'na', ); // Anchorage, AK expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 61.2181, longitude: -149.9003), 'na', ); // San Juan, Puerto Rico expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 18.4655, longitude: -66.1057), 'na', ); }); test('resolves Israel (il) for Israeli coordinates', () { // Tel Aviv expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 32.0853, longitude: 34.7818), 'il', ); // Jerusalem expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 31.7683, longitude: 35.2137), 'il', ); // Haifa expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 32.7940, longitude: 34.9896), 'il', ); }); test('resolves Rest of World (row) for Europe, Asia, Australia, South America', () { // London, UK expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 51.5074, longitude: -0.1278), 'row', ); // Paris, France expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 48.8566, longitude: 2.3522), 'row', ); // Tokyo, Japan expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 35.6762, longitude: 139.6503), 'row', ); // Sydney, Australia expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: -33.8688, longitude: 151.2093), 'row', ); // Sao Paulo, Brazil expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: -23.5505, longitude: -46.6333), 'row', ); // Cape Town, South Africa expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: -33.9249, longitude: 18.4241), 'row', ); }); test('handles coordinate edge cases, NaN and antimeridian crossing safely', () { expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: double.nan, longitude: double.nan), 'row', ); expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 95.0, longitude: -100.0), 'row', ); // Western Aleutians crossing antimeridian (e.g. Attu Island at 52.9°N, 173.2°E) expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 52.9, longitude: 173.2), 'na', ); // Normalized longitude across antimeridian (-186.8° wraps to 173.2°E) expect( WazeOsintRoadAlertProvider.determineEnvironment(latitude: 52.9, longitude: -186.8), 'na', ); }); }); group('WazeOsintRoadAlertProvider Capability Gate & Network Isolation', () { test('isLiveNetworkAllowed is false by default in standard builds', () { expect(WazeOsintRoadAlertProvider.isLiveNetworkAllowed, isFalse); }); test('uninjected provider without capability gate fails closed (notConfigured & 0 requests)', () async { final defaultProvider = WazeOsintRoadAlertProvider(); // testConnection fails closed final status = await defaultProvider.testConnection(''); expect(status, RoadAlertProviderStatus.notConfigured); // getAlerts fails closed and returns empty without throwing final alerts = await defaultProvider.getAlerts( latitude: 35.3934, longitude: -83.9261, radiusMeters: 5000, ); expect(alerts, isEmpty); }); test('injected client allows testing and fixtures without live network', () async { final mockClient = MockClient((request) async { return http.Response(json.encode({'alerts': []}), 200); }); final testProvider = WazeOsintRoadAlertProvider(client: mockClient); final status = await testProvider.testConnection(''); expect(status, RoadAlertProviderStatus.connected); }); }); }