import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:motorcycle_dash/services/routes/route_providers.dart'; 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/routes/route_models.dart'; import 'package:motorcycle_dash/services/routes/route_provider.dart'; import 'package:motorcycle_dash/services/routes/valhalla_route_provider.dart'; import 'package:motorcycle_dash/services/routes/aws_location_route_provider.dart'; void main() { group('ValhallaRouteProvider Tests', () { test( 'throws authenticationRequired when token provider returns null', () async { final provider = ValhallaRouteProvider( backendUrl: 'http://localhost:3000', tokenProvider: () async => null, ); final stops = [ const RouteStop( id: 's1', displayName: 'Start', latitude: 37.77, longitude: -122.42, order: 0, ), const RouteStop( id: 's2', displayName: 'End', latitude: 37.80, longitude: -122.27, order: 1, ), ]; expect( () => provider.calculateRoute(stops), throwsA( isA().having( (e) => e.code, 'code', 'authenticationRequired', ), ), ); }, ); test('validates minimum 2 stops', () async { final provider = ValhallaRouteProvider( tokenProvider: () async => 'valid-mock-token', ); final stops = [ const RouteStop( id: 's1', displayName: 'Single Stop', latitude: 37.77, longitude: -122.42, order: 0, ), ]; expect( () => provider.calculateRoute(stops), throwsA( isA().having( (e) => e.code, 'code', 'invalidRequest', ), ), ); }); test('validates maximum 25 stops', () async { final provider = ValhallaRouteProvider( tokenProvider: () async => 'valid-mock-token', ); final stops = List.generate( 26, (i) => RouteStop( id: 's$i', displayName: 'Stop $i', latitude: 37.0 + i * 0.01, longitude: -122.0, order: i, ), ); expect( () => provider.calculateRoute(stops), throwsA( isA().having( (e) => e.code, 'code', 'invalidRequest', ), ), ); }); test( 'calculates 2-stop route and parses normalized result with precision 6 geometry', () async { final mockClient = MockClient((request) async { expect(request.url.path, '/api/routes/calculate'); expect(request.headers['authorization'], 'Bearer valid-test-token'); final payload = jsonDecode(request.body) as Map; expect((payload['locations'] as List).length, 2); // Return fixture for SF to Oakland return http.Response( jsonEncode({ 'provider': 'valhalla_stadia', 'encodedPolyline': 'gbr`gAnk{nhFwrx@om`H', 'distanceMeters': 18500.5, 'durationSeconds': 1320, 'warnings': ['Motorcycle route preview'], 'attribution': '© Stadia Maps', }), 200, headers: {'content-type': 'application/json'}, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async => 'valid-test-token', ); final stops = [ const RouteStop( id: 's1', displayName: 'SF', latitude: 37.7749, longitude: -122.4194, order: 0, ), const RouteStop( id: 's2', displayName: 'Oakland', latitude: 37.8044, longitude: -122.2712, order: 1, ), ]; final result = await provider.calculateRoute(stops); expect(result.isStraightLine, false); expect(result.estimatedDistanceMeters, 18500.5); expect(result.estimatedDurationSeconds, 1320); expect(result.geometry.length, 2); expect(result.geometry[0].latitude, closeTo(37.7749, 1e-4)); expect(result.geometry[1].latitude, closeTo(37.8044, 1e-4)); expect(result.warnings, contains('Motorcycle route preview')); }, ); test('decodes each route leg independently', () async { final mockClient = MockClient((request) async { return http.Response( jsonEncode({ 'encodedPolylines': [ 'gbr`gAnk{nhFwrx@om`H', 'gbr`gAnk{nhFwrx@om`H', ], 'distanceMeters': 1000, 'durationSeconds': 60, 'warnings': [], }), 200, headers: {'content-type': 'application/json'}, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async => 'valid-test-token', ); final result = await provider.calculateRoute([ const RouteStop( id: '1', displayName: '1', latitude: 37, longitude: -122, order: 0, ), const RouteStop( id: '2', displayName: '2', latitude: 37.1, longitude: -122, order: 1, ), const RouteStop( id: '3', displayName: '3', latitude: 37.2, longitude: -122, order: 2, ), ]); expect(result.geometry.length, 4); }); test( 'searches places and parses coordinates from search results', () async { final mockClient = MockClient((request) async { expect(request.url.path, '/api/places/search'); expect(request.url.queryParameters['query'], 'Alice Restaurant'); return http.Response( jsonEncode({ 'results': [ { 'providerId': 'place_123', 'primaryText': "Alice's Restaurant", 'secondaryText': 'Woodside, CA', 'formattedAddress': "Alice's Restaurant, Skyline Blvd, Woodside, CA", 'latitude': 37.3868, 'longitude': -122.2644, }, ], 'attribution': 'Powered by Stadia Maps', }), 200, headers: {'content-type': 'application/json'}, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async => 'valid-test-token', ); final results = await provider.searchPlaces('Alice Restaurant'); expect(results.length, 1); final place = results.first; expect(place.providerId, 'place_123'); expect(place.primaryText, "Alice's Restaurant"); expect(place.hasCoordinates, true); expect(place.latitude, 37.3868); expect(place.longitude, -122.2644); // Verify immediate resolution without second network call final resolved = place.toResolvedPlace(); expect(resolved, isNotNull); expect(resolved!.latitude, 37.3868); expect(resolved.displayName, "Alice's Restaurant"); }, ); test('resolves place details by placeId', () async { final mockClient = MockClient((request) async { expect(request.url.path, '/api/places/resolve'); expect(request.url.queryParameters['placeId'], 'gid_999'); return http.Response( jsonEncode({ 'providerId': 'gid_999', 'displayName': 'Mount Hamilton Observatory', 'formattedAddress': 'Mount Hamilton Rd, San Jose, CA', 'latitude': 37.3414, 'longitude': -121.6429, 'attribution': 'Powered by Stadia Maps', }), 200, headers: {'content-type': 'application/json'}, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async => 'valid-test-token', ); final resolved = await provider.resolvePlace('gid_999'); expect(resolved.providerId, 'gid_999'); expect(resolved.displayName, 'Mount Hamilton Observatory'); expect(resolved.latitude, 37.3414); expect(resolved.longitude, -121.6429); }); test('maps backend 404 to routeNotFound exception', () async { final mockClient = MockClient((request) async { return http.Response( jsonEncode({ 'error': 'routeNotFound', 'message': 'No route could be found', }), 404, headers: {'content-type': 'application/json'}, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async => 'valid-test-token', ); final stops = [ const RouteStop( id: '1', displayName: '1', latitude: 0, longitude: 0, order: 0, ), const RouteStop( id: '2', displayName: '2', latitude: 1, longitude: 1, order: 1, ), ]; expect( () => provider.calculateRoute(stops), throwsA( isA().having( (e) => e.code, 'code', 'routeNotFound', ), ), ); }); test('routing request after bootstrap sends Bearer token', () async { var tokenProviderCalled = false; final mockClient = MockClient((request) async { expect(request.headers['authorization'], 'Bearer my-bootstrap-token'); return http.Response( jsonEncode({ 'encodedPolyline': 'w~j_~A_{n~P', 'distanceMeters': 1234.5, 'durationSeconds': 600, 'warnings': [], }), 200, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async { tokenProviderCalled = true; return 'my-bootstrap-token'; }, ); final stops = [ const RouteStop( id: '1', displayName: '1', latitude: 10, longitude: 10, order: 0, ), const RouteStop( id: '2', displayName: '2', latitude: 20, longitude: 20, order: 1, ), ]; await provider.calculateRoute(stops); expect(tokenProviderCalled, true); }); test('search request after bootstrap sends Bearer token', () async { var tokenProviderCalled = false; final mockClient = MockClient((request) async { expect( request.headers['authorization'], 'Bearer search-bootstrap-token', ); return http.Response( jsonEncode({'results': [], 'attribution': 'Test'}), 200, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async { tokenProviderCalled = true; return 'search-bootstrap-token'; }, ); await provider.searchPlaces('cafe'); expect(tokenProviderCalled, true); }); test('resolve request after bootstrap sends Bearer token', () async { var tokenProviderCalled = false; final mockClient = MockClient((request) async { expect( request.headers['authorization'], 'Bearer resolve-bootstrap-token', ); return http.Response( jsonEncode({ 'providerId': 'place_123', 'displayName': 'A Place', 'latitude': 10.0, 'longitude': 10.0, }), 200, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async { tokenProviderCalled = true; return 'resolve-bootstrap-token'; }, ); await provider.resolvePlace('place_123'); expect(tokenProviderCalled, true); }); }); group('Route Provider Configuration and Activation Tests', () { test('configured backendUrl is honored', () { final customProvider = ValhallaRouteProvider( backendUrl: 'http://custom-host:8080', tokenProvider: () async => 'token', ); expect(customProvider.backendUrl, 'http://custom-host:8080'); }); test( 'activeRouteCalculationProvider resolves to AwsLocationRouteProvider by default', () { final container = ProviderContainer(); addTearDown(container.dispose); final activeProvider = container.read(activeRouteCalculationProvider); expect(activeProvider, isA()); expect(activeProvider.name, contains('Amazon')); }, ); test( 'provider errors remain explicit and do not silently fall back to straight line', () async { final mockClient = MockClient((request) async { return http.Response( jsonEncode({ 'error': 'providerUnavailable', 'message': 'Routing server unavailable', }), 503, ); }); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async => 'token', ); final stops = [ const RouteStop( id: '1', displayName: '1', latitude: 10, longitude: 10, order: 0, ), const RouteStop( id: '2', displayName: '2', latitude: 20, longitude: 20, order: 1, ), ]; expect( () => provider.calculateRoute(stops), throwsA( isA().having( (e) => e.code, 'code', 'providerUnavailable', ), ), ); }, ); test('falls back to fallbackProvider when primary relay fails', () async { final mockClient = MockClient((request) async { return http.Response('Internal Server Error', 500); }); final mockFallback = MockClient((request) async { if (request.url.path.contains('/v2/autocomplete')) { return http.Response( jsonEncode({ 'ResultItems': [ { 'PlaceId': 'fallback_place_1', 'Title': 'Fallback Place', 'Address': {'Label': '123 Fallback Rd'}, } ] }), 200, ); } return http.Response('Not Found', 404); }); final awsFallback = AwsLocationRouteProvider( apiKey: 'test-key', client: mockFallback, ); final provider = ValhallaRouteProvider( client: mockClient, tokenProvider: () async => 'token', fallbackProvider: awsFallback, ); final results = await provider.searchPlaces('Fallback'); expect(results.length, equals(1)); expect(results.first.primaryText, equals('Fallback Place')); }); }); }