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/map/map_provider_interface.dart'; import 'package:motorcycle_dash/services/routes/aws_location_route_provider.dart'; import 'package:motorcycle_dash/services/routes/route_models.dart'; import 'package:motorcycle_dash/services/routes/route_provider.dart'; void main() { group('AwsLocationRouteProvider Tests', () { test('instance properties and custom configuration', () { final provider = AwsLocationRouteProvider( apiKey: 'test-key', region: 'us-west-2', ); expect(provider.name, equals('Amazon AWS Location')); expect(provider.apiKey, equals('test-key')); expect(provider.region, equals('us-west-2')); }); test('searchPlaces returns empty list for queries shorter than 2 characters', () async { final provider = AwsLocationRouteProvider(); final results = await provider.searchPlaces('a'); expect(results, isEmpty); }); test('searchPlaces parses combined autocomplete and searchText results', () async { final mockClient = MockClient((request) async { if (request.url.path.contains('/v2/autocomplete')) { expect(request.url.queryParameters['key'], equals('test-key')); final body = jsonDecode(request.body); expect(body['QueryText'], equals('Atlanta')); return http.Response( jsonEncode({ 'ResultItems': [ { 'PlaceId': 'place_auto_1', 'Title': 'Atlanta, GA, USA', 'Address': {'Label': 'Atlanta, Fulton County, Georgia, USA'}, } ] }), 200, ); } else if (request.url.path.contains('/v2/search-text')) { return http.Response( jsonEncode({ 'ResultItems': [ { 'PlaceId': 'place_text_1', 'Title': 'Atlanta City Hall', 'Address': {'Label': '68 Mitchell St SW, Atlanta, GA 30303'}, 'Position': [-84.3902, 33.7490], } ] }), 200, ); } return http.Response('Not Found', 404); }); final provider = AwsLocationRouteProvider( apiKey: 'test-key', client: mockClient, ); final results = await provider.searchPlaces( 'Atlanta', nearLocation: const AppLatLng(33.7490, -84.3880), ); expect(results.length, equals(2)); // First is text search result with coordinates expect(results[0].providerId, equals('place_text_1')); expect(results[0].primaryText, equals('Atlanta City Hall')); expect(results[0].latitude, equals(33.7490)); expect(results[0].longitude, equals(-84.3902)); expect(results[0].hasCoordinates, isTrue); // Second is autocomplete result expect(results[1].providerId, equals('place_auto_1')); expect(results[1].primaryText, equals('Atlanta, GA, USA')); expect(results[1].hasCoordinates, isFalse); }); test('resolvePlace fetches place details and returns ResolvedPlace', () async { final mockClient = MockClient((request) async { expect(request.url.path, contains('/v2/place/place_123')); expect(request.url.queryParameters['key'], equals('test-key')); return http.Response( jsonEncode({ 'PlaceId': 'place_123', 'Title': 'Blue Ridge Parkway North End', 'Address': {'Label': 'Afton, VA 22920'}, 'Position': [-78.8608, 38.0315], }), 200, ); }); final provider = AwsLocationRouteProvider( apiKey: 'test-key', client: mockClient, ); final place = await provider.resolvePlace('place_123'); expect(place.providerId, equals('place_123')); expect(place.displayName, equals('Blue Ridge Parkway North End')); expect(place.formattedAddress, equals('Afton, VA 22920')); expect(place.latitude, equals(38.0315)); expect(place.longitude, equals(-78.8608)); }); test('resolvePlace throws on empty placeId or 404', () async { final mockClient = MockClient((request) async { return http.Response('Not Found', 404); }); final provider = AwsLocationRouteProvider( apiKey: 'test-key', client: mockClient, ); expect( () => provider.resolvePlace(' '), throwsA(isA().having((e) => e.code, 'code', 'invalidRequest')), ); expect( () => provider.resolvePlace('missing_id'), throwsA(isA().having((e) => e.code, 'code', 'placeNotFound')), ); }); test('calculateRoute requires at least 2 stops', () async { final provider = AwsLocationRouteProvider(); expect( () => provider.calculateRoute([ const RouteStop( id: '1', order: 0, displayName: 'Origin', latitude: 34.0, longitude: -84.0, ), ]), throwsA(isA().having((e) => e.code, 'code', 'invalidRequest')), ); }); test('calculateRoute computes route and returns parsed geometry', () async { final mockClient = MockClient((request) async { expect(request.url.path, contains('/v2/routes')); expect(request.url.queryParameters['key'], equals('test-key')); final body = jsonDecode(request.body); expect(body['TravelMode'], equals('Car')); expect(body['LegGeometryFormat'], equals('Simple')); expect(body['Origin'], equals([-84.0, 34.0])); expect(body['Destination'], equals([-84.5, 34.5])); return http.Response( jsonEncode({ 'Routes': [ { 'Summary': { 'Distance': 65432.1, 'Duration': 2700, }, 'Legs': [ { 'Geometry': { 'LineString': [ [-84.0, 34.0], [-84.25, 34.25], [-84.5, 34.5], ], }, }, ], }, ], }), 200, ); }); final provider = AwsLocationRouteProvider( apiKey: 'test-key', client: mockClient, ); final stops = [ const RouteStop( id: '1', order: 0, displayName: 'Start', latitude: 34.0, longitude: -84.0, ), const RouteStop( id: '2', order: 1, displayName: 'End', latitude: 34.5, longitude: -84.5, ), ]; final result = await provider.calculateRoute(stops); expect(result.estimatedDistanceMeters, equals(65432.1)); expect(result.estimatedDurationSeconds, equals(2700)); expect(result.geometry.length, equals(3)); expect(result.geometry[0].latitude, equals(34.0)); expect(result.geometry[0].longitude, equals(-84.0)); expect(result.geometry[2].latitude, equals(34.5)); expect(result.geometry[2].longitude, equals(-84.5)); expect(result.isStraightLine, isFalse); expect(result.providerName, equals('Amazon AWS Location')); // Fallback maneuvers synthesized from geometry expect(result.maneuvers.isNotEmpty, isTrue); expect(result.maneuvers.first.type, equals(ManeuverType.depart)); expect(result.maneuvers.last.type, equals(ManeuverType.arrive)); }); test('calculateRoute with RoutingOptions applies avoidHighways and TravelSteps maneuvers', () async { final mockClient = MockClient((request) async { final body = jsonDecode(request.body); expect(body['TravelModeOptions']['Car']['Avoid']['Highways'], isTrue); expect(body['OptimizeRoutingFor'], equals('ShortestRoute')); expect(body['LegAdditionalInputs'], contains('TravelSteps')); return http.Response( jsonEncode({ 'Routes': [ { 'Summary': { 'Distance': 15000.0, 'Duration': 900, }, 'Legs': [ { 'Geometry': { 'LineString': [ [-84.0, 34.0], [-84.05, 34.05], [-84.10, 34.10], ], }, 'VehicleLegDetails': { 'TravelSteps': [ { 'Instruction': 'Head north on Main St', 'Type': 'Depart', 'Distance': 5000.0, 'Duration': 300, 'GeometryOffset': 0, 'CurrentRoad': 'Main St', }, { 'Instruction': 'Turn right onto Canyon Rd', 'Type': 'Turn', 'Turn': 'Right', 'Distance': 10000.0, 'Duration': 600, 'GeometryOffset': 1, 'NextRoad': 'Canyon Rd', }, { 'Instruction': 'Arrive at destination', 'Type': 'Arrive', 'Distance': 0.0, 'Duration': 0, 'GeometryOffset': 2, }, ], }, }, ], }, ], }), 200, ); }); final provider = AwsLocationRouteProvider( apiKey: 'test-key', client: mockClient, ); final stops = [ const RouteStop(id: '1', displayName: 'A', latitude: 34.0, longitude: -84.0), const RouteStop(id: '2', displayName: 'B', latitude: 34.10, longitude: -84.10), ]; final result = await provider.calculateRoute( stops, options: const RoutingOptions( preference: RoutingPreference.shortest, avoidHighways: true, ), ); expect(result.maneuvers.length, equals(3)); expect(result.maneuvers[0].instruction, equals('Head north on Main St')); expect(result.maneuvers[0].type, equals(ManeuverType.depart)); expect(result.maneuvers[1].instruction, equals('Turn right onto Canyon Rd')); expect(result.maneuvers[1].type, equals(ManeuverType.turnRight)); expect(result.maneuvers[1].streetName, equals('Canyon Rd')); expect(result.maneuvers[2].type, equals(ManeuverType.arrive)); expect(result.formattedDuration, equals('15 min')); expect(result.curvatureMetric, isNotNull); }); test('calculateRoute with twisty preference requests alternatives and selects highest curvature', () async { final mockClient = MockClient((request) async { final body = jsonDecode(request.body); expect(body['MaxAlternatives'], equals(2)); expect(body['TravelModeOptions']['Car']['Avoid']['Highways'], isTrue); return http.Response( jsonEncode({ 'Routes': [ { // Route 1: Straight route 'Summary': {'Distance': 10000.0, 'Duration': 600}, 'Legs': [ { 'Geometry': { 'LineString': [ [-84.0, 34.0], [-84.05, 34.05], [-84.10, 34.10], ], }, }, ], }, { // Route 2: Highly twisty mountain alternative with sharp turns 'Summary': {'Distance': 12000.0, 'Duration': 900}, 'Legs': [ { 'Geometry': { 'LineString': [ [-84.0, 34.0], [-84.02, 34.01], [-84.01, 34.03], [-84.05, 34.04], [-84.03, 34.07], [-84.08, 34.08], [-84.10, 34.10], ], }, }, ], }, ], }), 200, ); }); final provider = AwsLocationRouteProvider( apiKey: 'test-key', client: mockClient, ); final stops = [ const RouteStop(id: '1', displayName: 'A', latitude: 34.0, longitude: -84.0), const RouteStop(id: '2', displayName: 'B', latitude: 34.10, longitude: -84.10), ]; final result = await provider.calculateRoute( stops, options: const RoutingOptions( preference: RoutingPreference.twisty, ), ); // Verify that Route 2 was chosen because it has more curvature expect(result.estimatedDistanceMeters, equals(12000.0)); expect(result.estimatedDurationSeconds, equals(900)); expect(result.geometry.length, equals(7)); expect(result.curvatureMetric, isNotNull); expect(result.curvatureMetric, greaterThan(0)); }); test('calculateRoute extracts actionable error message on AWS API failure', () async { final mockClient = MockClient((request) async { return http.Response( jsonEncode({ 'message': 'AccessDeniedException: The provided API key is unauthorized for routes.geo.us-east-1.', }), 403, ); }); final provider = AwsLocationRouteProvider( apiKey: 'invalid-key', client: mockClient, ); final stops = [ const RouteStop(id: '1', displayName: 'A', latitude: 34.0, longitude: -84.0), const RouteStop(id: '2', displayName: 'B', latitude: 34.10, longitude: -84.10), ]; expect( () => provider.calculateRoute(stops), throwsA( isA() .having((e) => e.code, 'code', 'routeUnavailable') .having( (e) => e.message, 'message', contains('AccessDeniedException: The provided API key is unauthorized'), ), ), ); }); }); }