import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/features/routes/route_planner_screen.dart'; import 'package:motorcycle_dash/services/auth/auth_providers.dart'; import 'package:motorcycle_dash/services/auth/auth_user.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_providers.dart'; import 'package:motorcycle_dash/services/map/unified_map_view.dart'; import 'package:motorcycle_dash/services/routes/route_models.dart'; import 'package:motorcycle_dash/services/routes/route_providers.dart'; import 'fake_route_provider.dart'; void main() { group('RFR-1: Route Ordering & Geometry Unit Tests', () { test('Coordinate validation rejects invalid, NaN, Infinity, and (0,0)', () { expect(RoutePlannerNotifier.isValidCoordinate(37.7749, -122.4194), isTrue); expect(RoutePlannerNotifier.isValidCoordinate(-90.0, -180.0), isTrue); expect(RoutePlannerNotifier.isValidCoordinate(90.0, 180.0), isTrue); // Invalid bounds expect(RoutePlannerNotifier.isValidCoordinate(91.0, 0.0), isFalse); expect(RoutePlannerNotifier.isValidCoordinate(-91.0, 0.0), isFalse); expect(RoutePlannerNotifier.isValidCoordinate(0.0, 181.0), isFalse); expect(RoutePlannerNotifier.isValidCoordinate(0.0, -181.0), isFalse); // Null / NaN / Infinity expect(RoutePlannerNotifier.isValidCoordinate(null, -122.0), isFalse); expect(RoutePlannerNotifier.isValidCoordinate(37.0, null), isFalse); expect(RoutePlannerNotifier.isValidCoordinate(double.nan, -122.0), isFalse); expect(RoutePlannerNotifier.isValidCoordinate(37.0, double.nan), isFalse); expect(RoutePlannerNotifier.isValidCoordinate(double.infinity, -122.0), isFalse); expect(RoutePlannerNotifier.isValidCoordinate(37.0, double.infinity), isFalse); // (0, 0) placeholder expect(RoutePlannerNotifier.isValidCoordinate(0.0, 0.0), isFalse); }); test('RoutePlannerNotifier: start + destination triggers road calculation', () async { final fake = FakeRouteProvider(); final container = ProviderContainer( overrides: [ activeRouteCalculationProvider.overrideWithValue(fake), ], ); addTearDown(container.dispose); container.listen(routePlannerProvider(null), (_, _) {}); final notifier = container.read(routePlannerProvider(null).notifier); final origin = RouteStop( id: 'stop-1', displayName: 'Start Location', latitude: 37.7749, longitude: -122.4194, order: 0, type: RouteStopType.origin, ); final destination = RouteStop( id: 'stop-2', displayName: 'Destination Location', latitude: 37.3382, longitude: -121.8863, order: 1, type: RouteStopType.destination, ); notifier.addStop(stop: origin); await Future.delayed(const Duration(milliseconds: 50)); expect(container.read(routePlannerProvider(null)).stops.length, 1); expect(container.read(routePlannerProvider(null)).geometry, isEmpty); notifier.addStop(stop: destination); await Future.delayed(const Duration(milliseconds: 100)); final state = container.read(routePlannerProvider(null)); expect(state.stops.length, 2); expect(state.geometry.isNotEmpty, isTrue); expect(state.geometry.length, 2); expect(state.geometry.first.latitude, closeTo(37.7749, 0.001)); expect(state.geometry.last.latitude, closeTo(37.3382, 0.001)); }); test('RoutePlannerNotifier: start + stop + destination preserves exact canonical order', () async { final fake = FakeRouteProvider(); final container = ProviderContainer( overrides: [ activeRouteCalculationProvider.overrideWithValue(fake), ], ); addTearDown(container.dispose); container.listen(routePlannerProvider(null), (_, _) {}); final notifier = container.read(routePlannerProvider(null).notifier); final origin = RouteStop( id: 'origin', displayName: 'San Francisco', latitude: 37.7749, longitude: -122.4194, order: 0, type: RouteStopType.origin, ); final destination = RouteStop( id: 'destination', displayName: 'San Jose', latitude: 37.3382, longitude: -121.8863, order: 1, type: RouteStopType.destination, ); notifier.addStop(stop: origin); notifier.addStop(stop: destination); await Future.delayed(const Duration(milliseconds: 100)); // Intermediate stop added: Must be placed BEFORE destination, NOT after final intermediate = RouteStop( id: 'mid', displayName: 'Palo Alto', latitude: 37.4419, longitude: -122.1430, order: 2, type: RouteStopType.waypoint, ); notifier.addStop(stop: intermediate); await Future.delayed(const Duration(milliseconds: 100)); final state = container.read(routePlannerProvider(null)); expect(state.stops.length, 3); // Canonical order: origin (0) -> intermediate (1) -> destination (2) expect(state.stops[0].id, 'origin'); expect(state.stops[0].order, 0); expect(state.stops[0].type, RouteStopType.origin); expect(state.stops[1].id, 'mid'); expect(state.stops[1].order, 1); expect(state.stops[1].type, RouteStopType.waypoint); expect(state.stops[2].id, 'destination'); expect(state.stops[2].order, 2); expect(state.stops[2].type, RouteStopType.destination); // FakeRouteProvider received ordered stops expect(fake.lastCapturedStops, isNotNull); expect(fake.lastCapturedStops!.map((s) => s.id).toList(), ['origin', 'mid', 'destination']); // Geometry has all 3 points in order expect(state.geometry.length, 3); expect(state.geometry[0].latitude, closeTo(37.7749, 0.001)); expect(state.geometry[1].latitude, closeTo(37.4419, 0.001)); expect(state.geometry[2].latitude, closeTo(37.3382, 0.001)); }); test('RoutePlannerNotifier: multiple stops preserve order', () async { final fake = FakeRouteProvider(); final container = ProviderContainer( overrides: [ activeRouteCalculationProvider.overrideWithValue(fake), ], ); addTearDown(container.dispose); container.listen(routePlannerProvider(null), (_, _) {}); final notifier = container.read(routePlannerProvider(null).notifier); notifier.addStop(stop: const RouteStop(id: 'start', displayName: 'Start', latitude: 37.0, longitude: -122.0, order: 0, type: RouteStopType.origin)); notifier.addStop(stop: const RouteStop(id: 'end', displayName: 'End', latitude: 37.9, longitude: -122.9, order: 1, type: RouteStopType.destination)); notifier.addStop(stop: const RouteStop(id: 'stop1', displayName: 'Stop 1', latitude: 37.2, longitude: -122.2, order: 2, type: RouteStopType.waypoint)); notifier.addStop(stop: const RouteStop(id: 'stop2', displayName: 'Stop 2', latitude: 37.4, longitude: -122.4, order: 3, type: RouteStopType.waypoint)); await Future.delayed(const Duration(milliseconds: 100)); final stops = container.read(routePlannerProvider(null)).stops; expect(stops.length, 4); expect(stops.map((s) => s.id).toList(), ['start', 'stop1', 'stop2', 'end']); for (int i = 0; i < stops.length; i++) { expect(stops[i].order, i); } }); test('RoutePlannerNotifier: stop addition invalidates/replaces stale geometry', () async { final fake = FakeRouteProvider(); final container = ProviderContainer( overrides: [ activeRouteCalculationProvider.overrideWithValue(fake), ], ); addTearDown(container.dispose); container.listen(routePlannerProvider(null), (_, _) {}); final notifier = container.read(routePlannerProvider(null).notifier); notifier.addStop(stop: const RouteStop(id: 'A', displayName: 'A', latitude: 37.0, longitude: -122.0, order: 0, type: RouteStopType.origin)); notifier.addStop(stop: const RouteStop(id: 'B', displayName: 'B', latitude: 38.0, longitude: -122.0, order: 1, type: RouteStopType.destination)); await Future.delayed(const Duration(milliseconds: 100)); expect(container.read(routePlannerProvider(null)).geometry.length, 2); // Adding another stop must update geometry to length 3 notifier.addStop(stop: const RouteStop(id: 'C', displayName: 'C', latitude: 37.5, longitude: -122.0, order: 2, type: RouteStopType.waypoint)); await Future.delayed(const Duration(milliseconds: 100)); expect(container.read(routePlannerProvider(null)).geometry.length, 3); }); test('RoutePlannerNotifier: route calculation failure clears stale geometry and sets error', () async { final fake = FakeRouteProvider(shouldFail: true); final container = ProviderContainer( overrides: [ activeRouteCalculationProvider.overrideWithValue(fake), ], ); addTearDown(container.dispose); container.listen(routePlannerProvider(null), (_, _) {}); final notifier = container.read(routePlannerProvider(null).notifier); notifier.addStop(stop: const RouteStop(id: 'A', displayName: 'A', latitude: 37.0, longitude: -122.0, order: 0, type: RouteStopType.origin)); notifier.addStop(stop: const RouteStop(id: 'B', displayName: 'B', latitude: 38.0, longitude: -122.0, order: 1, type: RouteStopType.destination)); await Future.delayed(const Duration(milliseconds: 100)); final state = container.read(routePlannerProvider(null)); // Stale geometry MUST be cleared on failure expect(state.geometry, isEmpty); expect(state.routeCalculationError, isNotNull); expect(state.routeCalculationError, contains('Fake route not found')); }); test('No fallback coordinate contamination: rejected invalid stop', () { final fake = FakeRouteProvider(); final container = ProviderContainer( overrides: [ activeRouteCalculationProvider.overrideWithValue(fake), ], ); addTearDown(container.dispose); container.listen(routePlannerProvider(null), (_, _) {}); final notifier = container.read(routePlannerProvider(null).notifier); // Try adding NaN or 0,0 stop notifier.addStop(stop: const RouteStop(id: 'bad', displayName: 'Bad Stop', latitude: 0.0, longitude: 0.0, order: 0, type: RouteStopType.waypoint)); expect(container.read(routePlannerProvider(null)).stops, isEmpty); expect(container.read(routePlannerProvider(null)).stops, isEmpty); }); }); group('RFR-1: Widget & Theme Tests', () { testWidgets('Renderer receives road geometry polyline when route exists', (tester) async { await tester.binding.setSurfaceSize(const Size(412, 892)); // Pixel 8a portrait final fake = FakeRouteProvider(); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => const AuthUser(uid: 'alice', isAnonymous: false)), isProSubscriberProvider.overrideWith((ref) => false), activeRouteCalculationProvider.overrideWithValue(fake), ], child: const MaterialApp( home: RoutePlannerScreen(), ), ), ); await tester.pumpAndSettle(); // Initially no polylines rendered final initialMap = tester.widget(find.byKey(const ValueKey('route_planner_map_compact'))); expect(initialMap.polylines, isEmpty); // Add two stops await tester.tap(find.text('ADD STOP')); await tester.pumpAndSettle(); final textFields = find.descendant(of: find.byType(AlertDialog), matching: find.byType(TextField)); await tester.enterText(textFields.at(0), 'Start Point'); await tester.enterText(textFields.at(1), '37.7749'); await tester.enterText(textFields.at(2), '-122.4194'); await tester.tap(find.text('ADD')); await tester.pumpAndSettle(); // Add second stop await tester.tap(find.text('ADD STOP')); await tester.pumpAndSettle(); final textFields2 = find.descendant(of: find.byType(AlertDialog), matching: find.byType(TextField)); await tester.enterText(textFields2.at(0), 'End Point'); await tester.enterText(textFields2.at(1), '37.3382'); await tester.enterText(textFields2.at(2), '-121.8863'); await tester.tap(find.text('ADD')); await tester.pumpAndSettle(); // Verify road geometry polyline was passed to UnifiedMapView final updatedMap = tester.widget(find.byKey(const ValueKey('route_planner_map_compact'))); expect(updatedMap.polylines.length, 1); expect(updatedMap.polylines.first.points.length, 2); expect(updatedMap.polylines.first.points.first.latitude, closeTo(37.7749, 0.001)); expect(updatedMap.polylines.first.points.last.latitude, closeTo(37.3382, 0.001)); }); testWidgets('Light mode controls remain visible with proper contrast', (tester) async { await tester.binding.setSurfaceSize(const Size(412, 892)); final fake = FakeRouteProvider(); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => const AuthUser(uid: 'alice', isAnonymous: false)), isProSubscriberProvider.overrideWith((ref) => false), activeRouteCalculationProvider.overrideWithValue(fake), ], child: MaterialApp( theme: ThemeData.light(), home: const RoutePlannerScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('ROUTE PLANNER'), findsOneWidget); expect(find.text('SAVE'), findsOneWidget); expect(find.text('ADD STOP'), findsOneWidget); expect(find.text('CURRENT GPS'), findsOneWidget); expect(find.text('No stops added yet.'), findsOneWidget); // Open Add Stop dialog in light mode await tester.tap(find.text('ADD STOP')); await tester.pumpAndSettle(); expect(find.text('Add Route Stop'), findsOneWidget); expect(find.text('CANCEL'), findsOneWidget); expect(find.text('ADD'), findsOneWidget); // Cancel dialog await tester.tap(find.text('CANCEL')); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets('Dark mode controls render properly', (tester) async { await tester.binding.setSurfaceSize(const Size(412, 892)); final fake = FakeRouteProvider(); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => const AuthUser(uid: 'alice', isAnonymous: false)), isProSubscriberProvider.overrideWith((ref) => false), activeRouteCalculationProvider.overrideWithValue(fake), ], child: MaterialApp( theme: ThemeData.dark(), home: const RoutePlannerScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('ROUTE PLANNER'), findsOneWidget); expect(find.text('SAVE'), findsOneWidget); expect(find.text('ADD STOP'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('Pixel 8a portrait has no RenderFlex overflow with multiple stops and route info', (tester) async { // Pixel 8a exact viewport: 412 x 892 dp await tester.binding.setSurfaceSize(const Size(412, 892)); final fake = FakeRouteProvider(); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => const AuthUser(uid: 'alice', isAnonymous: false)), isProSubscriberProvider.overrideWith((ref) => false), activeRouteCalculationProvider.overrideWithValue(fake), ], child: const MaterialApp( home: RoutePlannerScreen(), ), ), ); await tester.pumpAndSettle(); final titleField = find.byType(TextField).first; await tester.enterText(titleField, 'Pixel 8a Full Test'); await tester.pumpAndSettle(); expect(find.text('Pixel 8a Full Test'), findsOneWidget); expect(tester.takeException(), isNull); }); }); }