import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:smartplus_delivery_app/core/api_client.dart'; import 'package:smartplus_delivery_app/modelos/delivery_order.dart'; import 'package:smartplus_delivery_app/servicios/delivery_repository.dart'; import 'package:smartplus_delivery_app/servicios/location_service.dart'; import 'package:smartplus_delivery_app/servicios/offline_queue.dart'; class _FakeLocationService extends LocationService { String? startedRouteId; @override Future requestPermission() async => true; @override Future startNativeTracking(String routeId) async { startedRouteId = routeId; return true; } } class _FakeClient extends http.BaseClient { bool failNext = true; bool offline = false; int requests = 0; int responseStatus = 200; final List> requestBodies = []; Map responseBody = {'success': true}; @override Future send(http.BaseRequest request) async { requests++; if (request is http.Request && request.body.isNotEmpty) { final decoded = jsonDecode(request.body); if (decoded is Map) { requestBodies.add(decoded.cast()); } } if (offline || failNext) { failNext = false; throw const SocketException('offline'); } final body = request.method == 'GET' && request.url.path.contains('/routes/current') ? { 'success': true, 'data': {'id': 'route-1', 'status': 'active'}, } : responseBody; return http.StreamedResponse( Stream.value(utf8.encode(jsonEncode(body))), responseStatus, request: request, ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('deduplicates offline events by UUID', () async { SharedPreferences.setMockInitialValues({}); final queue = OfflineQueue(); await queue.enqueue({'operation': 'accept_order', 'event_id': 'uuid-1'}); await queue.enqueue({'operation': 'accept_order', 'event_id': 'uuid-1'}); expect(await queue.count(), 1); expect((await queue.pending()).single['event_id'], 'uuid-1'); }); test('allows local workday shutdown when the tenant gate returns 403', () async { SharedPreferences.setMockInitialValues({ 'delivery_workday_tracking_enabled': true, }); final client = _FakeClient() ..failNext = false ..responseStatus = 403 ..responseBody = {'message': 'El reparto no esta habilitado.'}; final repository = DeliveryRepository(client: ApiClient(client: client)); await repository.stopWorkdayTracking(); final prefs = await SharedPreferences.getInstance(); expect(prefs.getBool('delivery_workday_tracking_enabled'), isNull); }); test('queues and replays an order action after connection returns', () async { SharedPreferences.setMockInitialValues({}); final queue = OfflineQueue(); final client = _FakeClient(); final repository = DeliveryRepository(client: ApiClient(client: client)); await expectLater( repository.acceptOrder('order-1'), throwsA(isA()), ); expect(await queue.count(), 1); final synced = await repository.syncPending(); expect(synced, 1); expect(await queue.count(), 0); expect(client.requests, 3); }); test( 'starts native tracking after an offline acceptance is synchronized', () async { SharedPreferences.setMockInitialValues({}); final queue = OfflineQueue(); final client = _FakeClient(); final location = _FakeLocationService(); final repository = DeliveryRepository( client: ApiClient(client: client), locationService: location, ); await expectLater( repository.acceptOrder('order-route-1'), throwsA(isA()), ); await repository.syncPending(); expect(await queue.count(), 0); expect(location.startedRouteId, 'route-1'); }, ); test('starts native tracking after an online acceptance', () async { SharedPreferences.setMockInitialValues({}); final client = _FakeClient()..failNext = false; final location = _FakeLocationService(); final repository = DeliveryRepository( client: ApiClient(client: client), locationService: location, ); await repository.acceptOrder('order-online-route-1'); expect(location.startedRouteId, 'route-1'); }); test('keeps delivery item quantities in the offline payload', () async { SharedPreferences.setMockInitialValues({}); final queue = OfflineQueue(); final client = _FakeClient()..offline = true; final repository = DeliveryRepository(client: ApiClient(client: client)); await expectLater( repository.deliverOrder( 'order-items-1', items: [ { 'item_id': 7, 'requested_quantity': 2, 'delivered_quantity': 3, 'additional_quantity': 1, 'rejected_quantity': 0, 'returned_quantity': 0, }, ], ), throwsA(isA()), ); expect(await queue.count(), 1); expect(client.requestBodies.single['items'], isA()); expect( (client.requestBodies.single['items'] as List) .single['delivered_quantity'], 3, ); }); test('queues status, GPS delivery and cash payment while offline', () async { SharedPreferences.setMockInitialValues({}); final queue = OfflineQueue(); final client = _FakeClient()..offline = true; final repository = DeliveryRepository(client: ApiClient(client: client)); await expectLater( repository.updateStatus( 'order-2', OrderStatus.notDelivered, observation: 'Cliente ausente', ), throwsA(isA()), ); await expectLater( repository.deliverOrder( 'order-2', latitude: -5.6369, longitude: -78.5390, accuracy: 8, ), throwsA(isA()), ); await expectLater( repository.reportPayment( 'order-2', PaymentMethod.cash, receivedAmount: 10, ), throwsA(isA()), ); expect(await queue.count(), 3); client.offline = false; expect(await repository.syncPending(), 3); expect(await queue.count(), 0); expect( client.requestBodies.any((body) => body['status'] == 'not_delivered'), isTrue, ); }); test( 'replays legacy camel-case status values using the API contract', () async { SharedPreferences.setMockInitialValues({ 'delivery_offline_events': [ jsonEncode({ 'operation': 'update_status', 'id': 'order-legacy', 'event_id': 'legacy-status-1', 'status': 'notDelivered', 'observation': 'Cliente ausente', }), ], }); final client = _FakeClient()..failNext = false; final repository = DeliveryRepository(client: ApiClient(client: client)); expect(await repository.syncPending(), 1); expect(await OfflineQueue().count(), 0); expect(client.requestBodies.single['status'], 'not_delivered'); }, ); test('queues fallback GPS points while offline', () async { SharedPreferences.setMockInitialValues({}); final queue = OfflineQueue(); final client = _FakeClient()..offline = true; final repository = DeliveryRepository(client: ApiClient(client: client)); await repository.sendLocation('route-1', -5.6369, -78.5390, 12); expect(await queue.count(), 1); client.offline = false; expect(await repository.syncPending(), 1); expect(await queue.count(), 0); }); test('queues cash settlement with an idempotent operation UUID', () async { SharedPreferences.setMockInitialValues({}); final queue = OfflineQueue(); final client = _FakeClient()..offline = true; final repository = DeliveryRepository(client: ApiClient(client: client)); await expectLater( repository.submitCashSettlement( routeId: 'route-cash-1', receivedCash: 18.50, observation: 'Entregado a caja', ), throwsA(isA()), ); expect(await queue.count(), 1); final pending = (await queue.pending()).single; expect(pending['operation'], 'route_cash_settle'); expect(pending['operation_uuid'], isA()); expect(pending['received_cash'], 18.50); client.offline = false; expect(await repository.syncPending(), 1); expect(await queue.count(), 0); }); test( 'does not accept a delivery response with an unexpected status', () async { SharedPreferences.setMockInitialValues({}); final client = _FakeClient() ..failNext = false ..responseBody = { 'success': true, 'data': {'delivery_status': 'en_route'}, }; final repository = DeliveryRepository(client: ApiClient(client: client)); await expectLater( repository.deliverOrder( 'order-3', latitude: -5.6369, longitude: -78.5390, accuracy: 8, ), throwsA(isA()), ); expect(await OfflineQueue().count(), 0); }, ); test( 'keeps an offline action when the server returns success false', () async { SharedPreferences.setMockInitialValues({}); final queue = OfflineQueue(); final client = _FakeClient()..offline = true; final repository = DeliveryRepository(client: ApiClient(client: client)); await expectLater( repository.acceptOrder('order-4'), throwsA(isA()), ); client.offline = false; client.responseBody = { 'success': false, 'message': 'TodavĂ­a no disponible', }; expect(await repository.syncPending(), 0); expect(await queue.count(), 1); }, ); }