import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/waze_road_alert_provider.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() async { SharedPreferences.setMockInitialValues({}); }); group('WazeRoadAlertProvider Metadata Tests', () { test('provider has correct id, name, and supported categories', () { final provider = WazeRoadAlertProvider(); expect(provider.id, equals('waze')); expect(provider.name, equals('Waze Police Alerts')); expect( provider.supportedCategories, equals({RoadAlertCategory.police}), ); }); }); group('WazeRoadAlertProvider Connection Tests', () { test('testConnection returns notConfigured for empty or whitespace key', () async { final provider = WazeRoadAlertProvider(apiHost: 'waze-proxy.internal'); expect(await provider.testConnection(''), equals(RoadAlertProviderStatus.notConfigured)); expect(await provider.testConnection(' '), equals(RoadAlertProviderStatus.notConfigured)); }); test('testConnection returns connected on HTTP 200 with required headers and params', () async { final mockClient = MockClient((request) async { expect(request.url.host, equals('waze-proxy.internal')); expect(request.url.path, equals('/waze/alerts-and-jams')); expect(request.headers['x-api-key'], equals('test_waze_key')); expect(request.url.queryParameters['center'], equals('34.0522,-118.2437')); expect(request.url.queryParameters['radius_units'], equals('KM')); expect(request.url.queryParameters['max_alerts'], equals('1')); expect(request.url.queryParameters['max_jams'], equals('0')); expect(request.url.queryParameters['alert_types'], equals('POLICE')); return http.Response(json.encode({'status': 'success', 'data': {'alerts': []}}), 200); }); final provider = WazeRoadAlertProvider(client: mockClient, apiHost: 'waze-proxy.internal'); final status = await provider.testConnection('test_waze_key'); expect(status, equals(RoadAlertProviderStatus.connected)); }); test('testConnection returns authError on HTTP 401 or 403', () async { final mockClient401 = MockClient((request) async { return http.Response('{"message": "Unauthorized"}', 401); }); final provider401 = WazeRoadAlertProvider(client: mockClient401, apiHost: 'waze-proxy.internal'); expect(await provider401.testConnection('bad_key'), equals(RoadAlertProviderStatus.authError)); final mockClient403 = MockClient((request) async { return http.Response('{"message": "Forbidden"}', 403); }); final provider403 = WazeRoadAlertProvider(client: mockClient403, apiHost: 'waze-proxy.internal'); expect(await provider403.testConnection('forbidden_key'), equals(RoadAlertProviderStatus.authError)); }); test('testConnection returns rateLimited on HTTP 429', () async { final mockClient = MockClient((request) async { return http.Response('{"message": "Too Many Requests"}', 429); }); final provider = WazeRoadAlertProvider(client: mockClient, apiHost: 'waze-proxy.internal'); expect(await provider.testConnection('busy_key'), equals(RoadAlertProviderStatus.rateLimited)); }); test('testConnection returns networkError on HTTP 500 or network exception', () async { final mockClient500 = MockClient((request) async { return http.Response('{"message": "Internal Server Error"}', 500); }); final provider500 = WazeRoadAlertProvider(client: mockClient500, apiHost: 'waze-proxy.internal'); expect(await provider500.testConnection('key'), equals(RoadAlertProviderStatus.networkError)); final mockClientException = MockClient((request) async { throw http.ClientException('Connection reset by peer'); }); final providerEx = WazeRoadAlertProvider(client: mockClientException, apiHost: 'waze-proxy.internal'); expect(await providerEx.testConnection('key'), equals(RoadAlertProviderStatus.networkError)); }); }); group('WazeRoadAlertProvider Query & Parsing Tests', () { test('getAlerts returns empty list if apiKey is null or empty', () async { final provider = WazeRoadAlertProvider(); final r1 = await provider.getAlerts( latitude: 34.05, longitude: -118.25, radiusMeters: 1000, apiKey: null, ); expect(r1, isEmpty); final r2 = await provider.getAlerts( latitude: 34.05, longitude: -118.25, radiusMeters: 1000, apiKey: ' ', ); expect(r2, isEmpty); }); test('getAlerts enforces alert_types=POLICE, max_jams=0, and radius_units=KM', () async { final mockClient = MockClient((request) async { expect(request.url.host, equals('waze-proxy.internal')); expect(request.url.path, equals('/waze/alerts-and-jams')); expect(request.headers['x-api-key'], equals('custom_key_123')); expect(request.url.queryParameters['center'], equals('37.7749,-122.4194')); expect(request.url.queryParameters['radius'], equals('2.5')); expect(request.url.queryParameters['radius_units'], equals('KM')); expect(request.url.queryParameters['max_alerts'], equals('25')); expect(request.url.queryParameters['max_jams'], equals('0')); expect(request.url.queryParameters['alert_types'], equals('POLICE')); return http.Response( json.encode({ 'status': 'success', 'data': { 'alerts': [ { 'alert_id': '101', 'type': 'POLICE', 'subtype': 'POLICE_VISIBLE', 'latitude': 37.775, 'longitude': -122.419, 'street': 'Market St', 'city': 'San Francisco, CA', 'publish_datetime_utc': '2026-09-26T18:00:00.000Z', 'alert_confidence': 5, 'alert_reliability': 8, 'num_thumbs_up': 4, }, { 'alert_id': '102', 'type': 'POLICE', 'subtype': 'POLICE_HIDDEN', 'latitude': 37.778, 'longitude': -122.421, 'street': 'Mission St', 'city': 'San Francisco, CA', 'publish_datetime_utc': '2026-09-26T18:05:00.000Z', 'alert_confidence': 4, 'alert_reliability': 7, 'num_thumbs_up': 1, }, // Unrelated category that must be strictly discarded { 'alert_id': '999', 'type': 'HAZARD', 'subtype': 'HAZARD_ON_SHOULDER', 'latitude': 37.780, 'longitude': -122.425, } ] } }), 200, ); }); final provider = WazeRoadAlertProvider(client: mockClient, apiHost: 'waze-proxy.internal'); final alerts = await provider.getAlerts( latitude: 37.7749, longitude: -122.4194, radiusMeters: 2500, apiKey: 'custom_key_123', ); // Only the 2 police alerts are parsed; hazard is strictly dropped expect(alerts.length, equals(2)); final visibleTrap = alerts[0]; expect(visibleTrap.id, equals('waze_101')); expect(visibleTrap.category, equals(RoadAlertCategory.police)); expect(visibleTrap.severity, equals(RoadAlertSeverity.medium)); expect(visibleTrap.description, contains('Police speed trap reported on Market St')); expect(visibleTrap.roadName, equals('Market St, San Francisco, CA')); expect(visibleTrap.source, equals('Waze')); final hiddenTrap = alerts[1]; expect(hiddenTrap.id, equals('waze_102')); expect(hiddenTrap.category, equals(RoadAlertCategory.police)); expect(hiddenTrap.severity, equals(RoadAlertSeverity.high)); expect(hiddenTrap.description, contains('Hidden police speed trap reported on Mission St')); }); test('parseWazeItem strictly rejects non-POLICE alerts', () { final provider = WazeRoadAlertProvider(); expect( provider.parseWazeItem({ 'alert_id': '1', 'type': 'ACCIDENT', 'latitude': 34.0, 'longitude': -118.0, }), isNull, ); expect( provider.parseWazeItem({ 'alert_id': '2', 'type': 'JAM', 'latitude': 34.0, 'longitude': -118.0, }), isNull, ); expect( provider.parseWazeItem({ 'alert_id': '3', 'type': 'HAZARD', 'latitude': 34.0, 'longitude': -118.0, }), isNull, ); expect( provider.parseWazeItem({ 'alert_id': '4', 'type': 'ROAD_CLOSED', 'latitude': 34.0, 'longitude': -118.0, }), isNull, ); }); test('parseWazeItem handles missing street or generic subtype smoothly', () { final provider = WazeRoadAlertProvider(); final alert = provider.parseWazeItem({ 'alert_id': '555', 'type': 'POLICE', 'latitude': 34.0522, 'longitude': -118.2437, }); expect(alert, isNotNull); expect(alert!.id, equals('waze_555')); expect(alert.category, equals(RoadAlertCategory.police)); expect(alert.description, equals('Police reported in area')); }); }); group('Riverpod Wiring for Waze Police Alerts Tests', () { test('parseWazeItem handles POLICE_WITH_MOBILE_CAMERA subtype', () { final provider = WazeRoadAlertProvider(); final alert = provider.parseWazeItem({ 'alert_id': 'camera_123', 'type': 'POLICE', 'subtype': 'POLICE_WITH_MOBILE_CAMERA', 'latitude': 34.078, 'longitude': -118.266, 'street': 'Sunset Blvd', 'city': 'Los Angeles', 'num_thumbs_up': 25, }); expect(alert, isNotNull); expect(alert!.id, equals('waze_camera_123')); expect(alert.category, equals(RoadAlertCategory.police)); expect(alert.severity, equals(RoadAlertSeverity.high)); expect(alert.description, equals('Police speed trap with camera reported on Sunset Blvd')); expect(alert.roadName, equals('Sunset Blvd, Los Angeles')); }); test('parseWazeItem handles POLICE_HIDING and POLICE_ON_OTHER_SIDE subtypes', () { final provider = WazeRoadAlertProvider(); final hidingAlert = provider.parseWazeItem({ 'alert_id': 'hiding_1', 'type': 'POLICE', 'subtype': 'POLICE_HIDING', 'latitude': 40.656, 'longitude': -74.009, 'street': '2nd Ave', 'city': 'New York', }); expect(hidingAlert, isNotNull); expect(hidingAlert!.severity, equals(RoadAlertSeverity.high)); expect(hidingAlert.description, contains('Hidden police speed trap reported on 2nd Ave')); final otherSideAlert = provider.parseWazeItem({ 'alert_id': 'side_2', 'type': 'POLICE', 'subtype': 'POLICE_ON_OTHER_SIDE', 'latitude': 40.658, 'longitude': -74.011, 'street': '3rd Ave', }); expect(otherSideAlert, isNotNull); expect(otherSideAlert!.severity, equals(RoadAlertSeverity.medium)); expect(otherSideAlert.description, contains('Police reported on opposite side of 3rd Ave')); }); test('getAlertsInBoundingBox sends bottom_left and top_right with POLICE filter', () async { final mockClient = MockClient((request) async { expect(request.url.host, equals('waze-proxy.internal')); expect(request.url.path, equals('/waze/alerts-and-jams')); expect(request.headers['x-api-key'], equals('bbox_key')); expect(request.url.queryParameters['bottom_left'], equals('40.6661,-74.1373')); expect(request.url.queryParameters['top_right'], equals('40.7727,-73.7681')); expect(request.url.queryParameters['max_alerts'], equals('15')); expect(request.url.queryParameters['max_jams'], equals('0')); expect(request.url.queryParameters['alert_types'], equals('POLICE')); return http.Response( json.encode({ 'status': 'OK', 'data': { 'alerts': [ { 'alert_id': 'ny_trap_1', 'type': 'POLICE', 'subtype': 'POLICE_HIDING', 'latitude': 40.700, 'longitude': -73.950, 'street': 'FDR Drive', } ] } }), 200, ); }); final provider = WazeRoadAlertProvider(client: mockClient, apiHost: 'waze-proxy.internal'); final alerts = await provider.getAlertsInBoundingBox( bottomLat: 40.6661, bottomLng: -74.1373, topLat: 40.7727, topLng: -73.7681, apiKey: 'bbox_key', maxAlerts: 15, ); expect(alerts.length, equals(1)); expect(alerts.first.id, equals('waze_ny_trap_1')); expect(alerts.first.category, equals(RoadAlertCategory.police)); expect(alerts.first.severity, equals(RoadAlertSeverity.high)); }); test('wazeApiKeyProvider initializes with empty default and does not expose bundled key', () async { final container = ProviderContainer(); addTearDown(container.dispose); final initialKey = container.read(wazeApiKeyProvider); expect(initialKey, isEmpty, reason: 'Bundled API key must not be present by default'); expect(initialKey, isNot(contains('ak_dud2s1987x5k7amfokrddptn41ov0iyf8kfdu3ljpv1fgvq'))); await container.read(wazeApiKeyProvider.notifier).setApiKey('custom_key_999'); expect(container.read(wazeApiKeyProvider), equals('custom_key_999')); }); test('wazeRoadAlertsEnabledProvider defaults to true and toggles', () async { final container = ProviderContainer(); addTearDown(container.dispose); expect(container.read(wazeRoadAlertsEnabledProvider), isTrue); await container.read(wazeRoadAlertsEnabledProvider.notifier).setEnabled(false); expect(container.read(wazeRoadAlertsEnabledProvider), isFalse); }); test('activeRoadAlertProviders excludes Waze when key is empty, includes when key is set', () async { final container = ProviderContainer(); addTearDown(container.dispose); // Initially no key, so Waze should not be active var activeList = container.read(activeRoadAlertProviders); expect(activeList.any((p) => p.id == 'waze'), isFalse); // After user sets an API key, Waze should be active await container.read(wazeApiKeyProvider.notifier).setApiKey('valid_test_key_123'); activeList = container.read(activeRoadAlertProviders); expect(activeList.any((p) => p.id == 'waze'), isTrue); }); test('WazeRoadAlertProvider uses custom apiHost if provided', () async { final mockClient = MockClient((request) async { expect(request.url.host, equals('custom-proxy.motorsignal.app')); return http.Response('{"status":"success","data":{"alerts":[]}}', 200); }); final provider = WazeRoadAlertProvider( client: mockClient, apiHost: 'custom-proxy.motorsignal.app', ); final status = await provider.testConnection('test_key'); expect(status, equals(RoadAlertProviderStatus.connected)); }); }); }