import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/community_traffic_parser.dart'; import 'package:motorcycle_dash/services/alerts/tomtom_road_alert_provider.dart'; import 'package:motorcycle_dash/services/alerts/mock_road_alert_provider.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() async { SharedPreferences.setMockInitialValues({}); }); group('Production Hardening Tests', () { // ─── Distance Calculation ──────────────────────────────────────────────── test('Distance calculation meters vs miles - no unit confusion', () { // 1 mile is ~1609 meters final distMeters = Geolocator.distanceBetween( 37.7749, -122.4194, // SF 37.7891, -122.4194, // ~1 mile north ); expect(distMeters, greaterThan(1500.0)); // must be in meters expect(distMeters, lessThan(1800.0)); // Convert to miles - must NOT be near raw meter value final distMiles = distMeters * 0.000621371; expect(distMiles, greaterThan(0.9)); expect(distMiles, lessThan(1.2)); expect(distMiles, isNot(closeTo(distMeters, 1.0))); }); // ─── Bounding Box Clamping ──────────────────────────────────────────────── test('TomTom provider does not generate invalid bounding box near poles', () { final provider = TomTomRoadAlertProvider(); // Near north pole - must not throw, must clamp latitudes expect(() async => await provider.getAlerts( latitude: 84.9, longitude: 0.0, radiusMeters: 500.0, ), returnsNormally); }); // ─── Alert Expiration ───────────────────────────────────────────────────── test('Expired alerts are not shown as current', () { final now = DateTime.now(); final expiredAlert = RoadAlert( id: 'exp_1', providerId: 'test', category: RoadAlertCategory.hazard, latitude: 37.7749, longitude: -122.4194, description: 'This alert has expired', reportedAt: now.subtract(const Duration(hours: 2)), expiresAt: now.subtract(const Duration(minutes: 30)), lastUpdated: now.subtract(const Duration(hours: 2)), source: 'Test', ); final liveAlert = RoadAlert( id: 'live_1', providerId: 'test', category: RoadAlertCategory.police, latitude: 37.7750, longitude: -122.4195, description: 'This alert is live', reportedAt: now.subtract(const Duration(minutes: 5)), expiresAt: now.add(const Duration(minutes: 25)), lastUpdated: now, source: 'Test', ); final alerts = [expiredAlert, liveAlert]; final activeAlerts = alerts.where((a) => a.expiresAt == null || a.expiresAt!.isAfter(now)).toList(); expect(activeAlerts.length, equals(1)); expect(activeAlerts.first.id, equals('live_1')); }); // ─── Deduplication ──────────────────────────────────────────────────────── test('Alerts are deduplicated by providerId + id', () { final now = DateTime.now(); RoadAlert makeAlert(String id) => RoadAlert( id: id, providerId: 'test_provider', category: RoadAlertCategory.accident, latitude: 37.7749, longitude: -122.4194, description: 'Accident', reportedAt: now, expiresAt: now.add(const Duration(hours: 1)), lastUpdated: now, source: 'Test', ); final alerts = [ makeAlert('acc_1'), makeAlert('acc_1'), // duplicate makeAlert('acc_2'), ]; final deduplicated = {}; for (var a in alerts) { final key = '${a.providerId}_${a.id}'; deduplicated[key] = a; } expect(deduplicated.length, equals(2)); }); // ─── Category Parsing ───────────────────────────────────────────────────── test('CommunityTrafficParser maps all police subtypes correctly', () { final inputs = ['POLICE', 'POLICE_VISIBLE', 'POLICE_HIDDEN', 'POLICE_TRAP']; for (final subtype in inputs) { final cat = CommunityTrafficParser.mapTypeToCategory('POLICE', subtype); expect(cat, equals(RoadAlertCategory.police), reason: 'Expected police for subtype $subtype'); } }); test('CommunityTrafficParser maps speed camera types correctly', () { expect(CommunityTrafficParser.mapTypeToCategory('SPEED_CAM', ''), equals(RoadAlertCategory.speedCamera)); expect(CommunityTrafficParser.mapTypeToCategory('', 'RED_LIGHT_CAMERA'), equals(RoadAlertCategory.speedCamera)); }); test('CommunityTrafficParser handles unknown type as other', () { final cat = CommunityTrafficParser.mapTypeToCategory('UNKNOWN_FUTURE_TYPE', ''); // Should not throw - should fall back to 'other' expect(cat, isNotNull); }); test('CommunityTrafficParser skips alerts with missing coordinates', () { final jsonMissingCoords = { 'alerts': [ {'id': 'no_coords', 'type': 'HAZARD', 'subtype': ''}, {'id': 'valid', 'type': 'ACCIDENT', 'subtype': '', 'latitude': 37.77, 'longitude': -122.41}, ] }; final alerts = CommunityTrafficParser.parseAlertsJson( jsonMissingCoords, providerId: 'test', providerName: 'Test', ); expect(alerts.length, equals(1)); expect(alerts.first.id, equals('valid')); }); // ─── Provider Capabilities ──────────────────────────────────────────────── test('TomTom does NOT advertise police or speed camera categories', () { final provider = TomTomRoadAlertProvider(); expect(provider.supportedCategories, isNot(contains(RoadAlertCategory.police))); expect(provider.supportedCategories, isNot(contains(RoadAlertCategory.speedCamera))); }); test('Mock provider returns empty in non-debug mode', () async { // This test can only verify in the current build mode final mockProvider = MockRoadAlertProvider(); if (!kDebugMode) { final alerts = await mockProvider.getAlerts( latitude: 37.7749, longitude: -122.4194, radiusMeters: 1000.0, ); expect(alerts.isEmpty, isTrue, reason: 'Mock provider must not expose alerts in release mode'); } }); // ─── Riverpod Settings Defaults ─────────────────────────────────────────── test('Road alerts settings have safe defaults', () { final container = ProviderContainer(); addTearDown(container.dispose); expect(container.read(roadAlertsEnabledProvider), isTrue); expect(container.read(audioAlertsEnabledProvider), isTrue); expect(container.read(alertRadiusMetersProvider), equals(804.7)); expect(container.read(tomtomApiKeyProvider), isEmpty); expect(container.read(communityApiKeyProvider), anyOf(isEmpty, contains('wz_live'))); expect(container.read(tomtomStatusProvider), equals(RoadAlertProviderStatus.notConfigured)); expect(container.read(communityStatusProvider), anyOf(equals(RoadAlertProviderStatus.notConfigured), equals(RoadAlertProviderStatus.configured))); }); test('Active providers list type is correct', () { // Just verify the provider definition is sane without reading SharedPreferences. // Full integration is verified on-device. expect(activeRoadAlertProviders, isNotNull); }); // ─── Alert Radius Conversions ───────────────────────────────────────────── test('Alert radius values match documented distances', () { const ft500 = 152.4; // 500 feet in meters const ft1000 = 304.8; const quarterMile = 402.3; const halfMile = 804.7; const oneMile = 1609.3; // Verify none are equal (distinct values) final radii = [ft500, ft1000, quarterMile, halfMile, oneMile]; final unique = radii.toSet(); expect(unique.length, equals(5)); // Verify ordering expect(ft500, lessThan(ft1000)); expect(ft1000, lessThan(quarterMile)); expect(quarterMile, lessThan(halfMile)); expect(halfMile, lessThan(oneMile)); }); }); }