import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/services/map/map_provider_interface.dart'; import 'package:motorcycle_dash/services/navigation/navigation_session_service.dart'; import 'package:motorcycle_dash/services/routes/route_models.dart'; import 'package:motorcycle_dash/services/routes/route_provider.dart'; class _TestRouteProvider implements RouteProvider { int calculateCount = 0; List? lastStops; RoutingOptions? lastOptions; @override String get name => 'Test Provider'; @override Future calculateRoute( List stops, { bool avoidHighways = false, bool avoidTolls = false, bool avoidFerries = false, RoutingOptions? options, }) async { calculateCount++; lastStops = stops; lastOptions = options; final maneuvers = [ RouteManeuver( index: 0, instruction: 'Head toward New Route St', type: ManeuverType.depart, distanceMeters: 400.0, durationSeconds: 45, location: stops.first.latLng, ), RouteManeuver( index: 1, instruction: 'Arrive at destination', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, location: stops.last.latLng, ), ]; return RouteCalculationResult( straightLineDistanceMeters: 1200.0, estimatedDistanceMeters: 1200.0, estimatedDurationSeconds: 150, geometry: [stops.first.latLng, stops.last.latLng], isStraightLine: false, providerName: name, maneuvers: maneuvers, options: options, ); } @override Future> searchPlaces( String query, { AppLatLng? nearLocation, }) async => []; @override Future resolvePlace(String providerId) async => throw UnimplementedError(); } void main() { group('NavigationSessionNotifier Tests', () { test('initial state is idle and empty', () { final notifier = NavigationSessionNotifier(); expect(notifier.state.status, equals(NavigationStatus.idle)); expect(notifier.state.isNavigating, isFalse); expect(notifier.state.hasActiveRoute, isFalse); expect(notifier.state.maneuvers, isEmpty); }); test('startNavigation initiates turn-by-turn guidance and records last announced instruction', () async { final notifier = NavigationSessionNotifier(); final maneuvers = [ const RouteManeuver( index: 0, instruction: 'Head north on Peachtree St', type: ManeuverType.depart, distanceMeters: 500.0, durationSeconds: 60, streetName: 'Peachtree St', location: AppLatLng(33.750, -84.388), ), const RouteManeuver( index: 1, instruction: 'Turn right on 10th St', type: ManeuverType.turnRight, distanceMeters: 1000.0, durationSeconds: 120, streetName: '10th St', location: AppLatLng(33.780, -84.385), ), const RouteManeuver( index: 2, instruction: 'Arrive at Piedmont Park', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, streetName: 'Piedmont Park', location: AppLatLng(33.790, -84.370), ), ]; final calcResult = RouteCalculationResult( straightLineDistanceMeters: 1500.0, estimatedDistanceMeters: 1500.0, estimatedDurationSeconds: 180, geometry: const [ AppLatLng(33.750, -84.388), AppLatLng(33.780, -84.385), AppLatLng(33.790, -84.370), ], isStraightLine: false, providerName: 'AWS Location', maneuvers: maneuvers, ); await notifier.startNavigation( calculationResult: calcResult, destination: const ResolvedPlace( providerId: 'dest_1', displayName: 'Piedmont Park', latitude: 33.790, longitude: -84.370, ), ); expect(notifier.state.isNavigating, isTrue); expect(notifier.state.status, equals(NavigationStatus.navigating)); expect(notifier.state.maneuvers.length, equals(3)); expect(notifier.state.currentManeuverIndex, equals(0)); expect(notifier.state.currentManeuver?.instruction, equals('Head north on Peachtree St')); expect(notifier.state.nextManeuver?.instruction, equals('Turn right on 10th St')); expect(notifier.state.remainingDistanceMeters, equals(1500.0)); expect(notifier.state.remainingDurationSeconds, equals(180)); }); test('updateLocation advances maneuvers when rider approaches waypoint', () async { final notifier = NavigationSessionNotifier(); final maneuvers = [ const RouteManeuver( index: 0, instruction: 'Head north on Main', type: ManeuverType.depart, distanceMeters: 200.0, durationSeconds: 30, location: AppLatLng(33.7500, -84.3880), ), const RouteManeuver( index: 1, instruction: 'Turn left on Elm', type: ManeuverType.turnLeft, distanceMeters: 800.0, durationSeconds: 90, location: AppLatLng(33.7518, -84.3880), ), const RouteManeuver( index: 2, instruction: 'Arrive at destination', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, location: AppLatLng(33.7518, -84.3960), ), ]; final calcResult = RouteCalculationResult( straightLineDistanceMeters: 1000.0, estimatedDistanceMeters: 1000.0, estimatedDurationSeconds: 120, geometry: const [ AppLatLng(33.7500, -84.3880), AppLatLng(33.7518, -84.3880), AppLatLng(33.7518, -84.3960), ], isStraightLine: false, providerName: 'AWS Location', maneuvers: maneuvers, ); await notifier.startNavigation(calculationResult: calcResult); // Stationary rider at departure does NOT prematurely advance to step 1 notifier.updateLocation(const AppLatLng(33.7500, -84.3880), speedMph: 0.0); expect(notifier.state.currentManeuverIndex, equals(0)); expect(notifier.state.currentManeuver?.instruction, equals('Head north on Main')); // Rider departs and is moving (> 3.0 mph), advancing to step 1 notifier.updateLocation(const AppLatLng(33.7500, -84.3880), speedMph: 25.0); expect(notifier.state.currentManeuverIndex, equals(1)); expect(notifier.state.currentManeuver?.instruction, equals('Turn left on Elm')); // Pause and resume notifier.pauseNavigation(); expect(notifier.state.status, equals(NavigationStatus.paused)); expect(notifier.state.isNavigating, isFalse); notifier.resumeNavigation(); expect(notifier.state.status, equals(NavigationStatus.navigating)); // Stop navigation await notifier.stopNavigation(); expect(notifier.state.status, equals(NavigationStatus.idle)); expect(notifier.state.hasActiveRoute, isFalse); }); test('off-route detection triggers genuine route recalculation with newly calculated maneuvers', () async { final testRouteProvider = _TestRouteProvider(); final notifier = NavigationSessionNotifier(routeProvider: testRouteProvider); final initialManeuvers = [ const RouteManeuver( index: 0, instruction: 'Head north on North Ave', type: ManeuverType.depart, distanceMeters: 500.0, durationSeconds: 60, location: AppLatLng(33.7500, -84.3880), ), const RouteManeuver( index: 1, instruction: 'Arrive at destination', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, location: AppLatLng(33.7600, -84.3880), ), ]; final initialResult = RouteCalculationResult( straightLineDistanceMeters: 1100.0, estimatedDistanceMeters: 1100.0, estimatedDurationSeconds: 90, geometry: const [ AppLatLng(33.7500, -84.3880), AppLatLng(33.7600, -84.3880), ], isStraightLine: false, providerName: 'Test Provider', maneuvers: initialManeuvers, ); await notifier.startNavigation( calculationResult: initialResult, destination: const ResolvedPlace( providerId: 'dest_park', displayName: 'Park', latitude: 33.7600, longitude: -84.3880, ), ); expect(notifier.state.isNavigating, isTrue); expect(testRouteProvider.calculateCount, equals(0)); // Rider deviates far off-route (> 500 meters east) const offRoutePos = AppLatLng(33.7550, -84.3750); notifier.updateLocation(offRoutePos, speedMph: 35.0); await Future.delayed(Duration.zero); // Verify recalculation was actually invoked on the RouteProvider expect(testRouteProvider.calculateCount, equals(1)); expect(testRouteProvider.lastStops?.first.latitude, equals(offRoutePos.latitude)); expect(testRouteProvider.lastStops?.first.longitude, equals(offRoutePos.longitude)); // Verify session updated with the new recalculation result and maneuvers expect(notifier.state.isNavigating, isTrue); expect(notifier.state.currentManeuver?.instruction, equals('Head toward New Route St')); expect(notifier.state.maneuvers.length, equals(2)); }); test('toggleTts toggles voice guidance', () { final notifier = NavigationSessionNotifier(); expect(notifier.state.ttsEnabled, isTrue); notifier.toggleTts(); expect(notifier.state.ttsEnabled, isFalse); notifier.toggleTts(); expect(notifier.state.ttsEnabled, isTrue); }); test('remaining distance strictly counts down and maneuvers advance as rider travels', () async { final notifier = NavigationSessionNotifier(); // Polyline with 5 points, ~1000m total const p0 = AppLatLng(33.7500, -84.3880); const p1 = AppLatLng(33.7525, -84.3880); // ~278m north const p2 = AppLatLng(33.7550, -84.3880); // ~278m north (turn right) const p3 = AppLatLng(33.7550, -84.3850); // ~278m east const p4 = AppLatLng(33.7550, -84.3820); // ~278m east (destination) final maneuvers = [ const RouteManeuver( index: 0, instruction: 'Depart north on Main St', type: ManeuverType.depart, distanceMeters: 556.0, durationSeconds: 60, location: p0, geometryOffset: 0, ), const RouteManeuver( index: 1, instruction: 'Turn right on Oak St', type: ManeuverType.turnRight, distanceMeters: 556.0, durationSeconds: 60, location: p2, geometryOffset: 2, ), const RouteManeuver( index: 2, instruction: 'Arrive at destination', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, location: p4, geometryOffset: 4, ), ]; final calcResult = RouteCalculationResult( straightLineDistanceMeters: 1000.0, estimatedDistanceMeters: 1112.0, estimatedDurationSeconds: 120, geometry: const [p0, p1, p2, p3, p4], isStraightLine: false, providerName: 'AWS Location', maneuvers: maneuvers, ); await notifier.startNavigation(calculationResult: calcResult); final initialRemaining = notifier.state.remainingDistanceMeters; expect(initialRemaining, greaterThan(1000.0)); // Rider moves to p1 (halfway to turn) notifier.updateLocation(p1, speedMph: 30.0); final remainingAtP1 = notifier.state.remainingDistanceMeters; // Remaining distance MUST decrease (never count up!) expect(remainingAtP1, lessThan(initialRemaining)); expect(notifier.state.currentManeuverIndex, equals(1)); expect(notifier.state.currentManeuver?.instruction, equals('Turn right on Oak St')); final distToTurnP1 = notifier.state.distanceToNextManeuverMeters; expect(distToTurnP1, greaterThan(200.0)); expect(distToTurnP1, lessThan(350.0)); // Rider approaches turn at p2 (within 10m of p2) notifier.updateLocation(p2, speedMph: 20.0); final remainingAtP2 = notifier.state.remainingDistanceMeters; expect(remainingAtP2, lessThan(remainingAtP1)); // Maneuver advances to arrival! expect(notifier.state.currentManeuverIndex, equals(2)); expect(notifier.state.currentManeuver?.instruction, equals('Arrive at destination')); // Rider continues to p3 notifier.updateLocation(p3, speedMph: 25.0); final remainingAtP3 = notifier.state.remainingDistanceMeters; expect(remainingAtP3, lessThan(remainingAtP2)); // Rider arrives at destination p4 notifier.updateLocation(p4, speedMph: 5.0); expect(notifier.state.status, equals(NavigationStatus.arrived)); expect(notifier.state.remainingDistanceMeters, equals(0.0)); }); }); }