// ignore_for_file: subtype_of_sealed_class import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_service.dart'; import 'package:motorcycle_dash/services/alerts/swarm_road_alert_service.dart'; import 'package:motorcycle_dash/services/alerts/widgets/quick_swarm_report_modal.dart'; import 'package:motorcycle_dash/services/group_rides/group_ride_providers.dart'; class _LocalAlerts implements RoadAlertService { final reports = []; @override void simulateAlert(RoadAlert alert) => reports.add(alert); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _Location extends GeolocatorPlatform { late final current = Completer(); Future Function()? lastKnown; @override Future getCurrentPosition({LocationSettings? locationSettings}) => current.future; @override Future getLastKnownPosition({ bool forceLocationManager = false, }) async => lastKnown == null ? null : await lastKnown!(); } // Delay only the Firestore acknowledgement boundary; use the production // SwarmRoadAlertService and an actual fake Firestore document underneath. class _DelayedFirestore extends FakeFirebaseFirestore { final gate = Completer(); int writes = 0; @override CollectionReference> collection(String path) => _DelayedCollection(super.collection(path), this); } class _DelayedCollection implements CollectionReference> { _DelayedCollection(this.delegate, this.firestore); final CollectionReference> delegate; @override final _DelayedFirestore firestore; @override DocumentReference> doc([String? path]) => _DelayedDocument(delegate.doc(path), firestore); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _DelayedDocument implements DocumentReference> { _DelayedDocument(this.delegate, this.firestore); final DocumentReference> delegate; @override final _DelayedFirestore firestore; @override Future set(Map data, [SetOptions? options]) async { firestore.writes++; await firestore.gate.future; await delegate.set(data, options); } @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } Position _position() => Position( latitude: 35.3934, longitude: -83.9261, timestamp: DateTime.now(), accuracy: 5, heading: 90, headingAccuracy: 1, altitude: 0, altitudeAccuracy: 1, speed: 0, speedAccuracy: 1, ); void main() { TestWidgetsFlutterBinding.ensureInitialized(); late _Location location; late _LocalAlerts alerts; late GeolocatorPlatform originalLocation; setUp(() { originalLocation = GeolocatorPlatform.instance; location = _Location(); GeolocatorPlatform.instance = location; alerts = _LocalAlerts(); }); tearDown(() => GeolocatorPlatform.instance = originalLocation); Future open(WidgetTester tester, {SwarmRoadAlertService? service}) async { await tester.pumpWidget( ProviderScope( overrides: [ activeUserGroupRideProvider.overrideWithValue(null), swarmRoadAlertServiceProvider.overrideWithValue( service ?? SwarmRoadAlertService(), ), roadAlertServiceProvider.overrideWithValue(alerts), ], child: MaterialApp( home: Scaffold( body: Builder( builder: (context) { return TextButton( onPressed: () => QuickSwarmReportModal.show(context), child: const Text('Open report'), ); }, ), ), ), ), ); await tester.tap(find.text('Open report')); await tester.pumpAndSettle(); } testWidgets('delayed GPS report survives the sheet exit animation', ( tester, ) async { await open(tester); await tester.tap(find.text('Road Hazard')); await tester.pumpAndSettle(); // Deliberately resolve after the original implementation disposed its ref. location.current.complete( Position( latitude: 35.3934, longitude: -83.9261, timestamp: DateTime.now(), accuracy: 5, heading: 90, headingAccuracy: 1, altitude: 0, altitudeAccuracy: 1, speed: 0, speedAccuracy: 1, ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.pumpAndSettle(); expect( find.textContaining("Cannot use 'ref' after the widget was disposed"), findsNothing, ); expect(alerts.reports, hasLength(1), reason: tester .widgetList(find.byType(Text)) .map((text) => text.data) .join(' | ')); expect(alerts.reports.single.category, RoadAlertCategory.hazard); expect(find.textContaining('Bad state'), findsNothing); expect(tester.takeException(), isNull); }); testWidgets('dismissal while GPS is pending cancels the report', ( tester, ) async { await open(tester); await tester.tap(find.text('Road Hazard')); await tester.pump(); await tester.tap(find.byIcon(Icons.close)); await tester.pumpAndSettle(); location.current.complete( Position( latitude: 35.3934, longitude: -83.9261, timestamp: DateTime.now(), accuracy: 5, heading: 90, headingAccuracy: 1, altitude: 0, altitudeAccuracy: 1, speed: 0, speedAccuracy: 1, ), ); await tester.pump(); expect(alerts.reports, isEmpty); expect(tester.takeException(), isNull); }); testWidgets('repeated taps create one report', (tester) async { await open(tester); await tester.tap(find.text('Road Hazard')); await tester.tap(find.text('Road Hazard')); await tester.pump(); expect( tester .widget( find.widgetWithText(ElevatedButton, 'Road Hazard'), ) .onPressed, isNull, ); location.current.complete( Position( latitude: 35.3934, longitude: -83.9261, timestamp: DateTime.now(), accuracy: 5, heading: 90, headingAccuracy: 1, altitude: 0, altitudeAccuracy: 1, speed: 0, speedAccuracy: 1, ), ); await tester.pumpAndSettle(); expect(alerts.reports, hasLength(1)); expect(tester.takeException(), isNull); }); testWidgets('missing location gives feedback without submitting', ( tester, ) async { await open(tester); await tester.tap(find.text('Road Hazard')); location.current.completeError(const PermissionDeniedException('denied')); await tester.pumpAndSettle(); expect(alerts.reports, isEmpty); expect( find.text('Cannot report: GPS location unavailable.'), findsOneWidget, ); expect(tester.takeException(), isNull); }); testWidgets('close cancels even before its exit animation finishes', (tester) async { await open(tester); await tester.tap(find.text('Road Hazard')); await tester.tap(find.byIcon(Icons.close)); location.current.complete(_position()); await tester.pumpAndSettle(); expect(alerts.reports, isEmpty); expect(tester.takeException(), isNull); }); testWidgets('GPS timeout falls back and missing GPS permits retry', (tester) async { await open(tester); await tester.tap(find.text('Road Hazard')); await tester.pump(const Duration(seconds: 5)); await tester.pumpAndSettle(); expect(alerts.reports, isEmpty); expect(find.text('Cannot report: GPS location unavailable.'), findsOneWidget); expect(tester.widget(find.widgetWithText( ElevatedButton, 'Road Hazard')).onPressed, isNotNull); location.current.complete(_position()); await tester.pumpAndSettle(); expect(alerts.reports, isEmpty); }); testWidgets('last-known timeout gives failure feedback', (tester) async { final lastKnown = Completer(); location.lastKnown = () => lastKnown.future; await open(tester); await tester.tap(find.text('Road Hazard')); location.current.completeError(StateError('GPS unavailable')); await tester.pump(); await tester.pump(const Duration(seconds: 2)); await tester.pumpAndSettle(); expect(find.textContaining('Failed to submit report:'), findsOneWidget); expect(alerts.reports, isEmpty); lastKnown.complete(null); await tester.pump(); }); testWidgets('confirmed cloud outcome has a persisted fake document', (tester) async { final firestore = FakeFirebaseFirestore(); await open(tester, service: SwarmRoadAlertService(firestore: firestore)); await tester.tap(find.text('Road Hazard')); location.current.complete(_position()); await tester.pumpAndSettle(); expect(alerts.reports, hasLength(1)); expect(find.textContaining('saved to cloud'), findsOneWidget); final docs = await firestore.collection('road_alerts').get(); expect(docs.docs, hasLength(1)); expect(docs.docs.single.data()['latitude'], 35.3934); }); testWidgets('rejected cloud write gives local-only failure feedback', (tester) async { final firestore = FakeFirebaseFirestore(securityRules: ''' service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read: if true; allow write: if false; } } } '''); await open(tester, service: SwarmRoadAlertService(firestore: firestore)); await tester.tap(find.text('Road Hazard')); location.current.complete(_position()); await tester.pumpAndSettle(); expect(alerts.reports, hasLength(1)); expect(find.textContaining('cloud delivery failed'), findsOneWidget); expect((await firestore.collection('road_alerts').get()).docs, isEmpty); }); testWidgets('pending acknowledgement returns locally without retrying write', (tester) async { final firestore = _DelayedFirestore(); await open(tester, service: SwarmRoadAlertService(firestore: firestore)); await tester.tap(find.text('Road Hazard')); location.current.complete(_position()); await tester.pump(); await tester.pump(const Duration(seconds: 6)); await tester.pumpAndSettle(); expect(alerts.reports, hasLength(1)); expect(find.textContaining('cloud delivery pending'), findsOneWidget); firestore.gate.complete(); await tester.pumpAndSettle(); expect(firestore.writes, 1); expect(alerts.reports, hasLength(1)); expect(tester.takeException(), isNull); }); testWidgets('removing provider scope during cloud write avoids disposed service', (tester) async { final firestore = _DelayedFirestore(); await open(tester, service: SwarmRoadAlertService(firestore: firestore)); await tester.tap(find.text('Road Hazard')); location.current.complete(_position()); await tester.pump(); expect(firestore.writes, 1); await tester.pumpWidget(const SizedBox.shrink()); firestore.gate.complete(); await tester.pumpAndSettle(); expect(alerts.reports, isEmpty); expect(tester.takeException(), isNull); }); testWidgets('BUG-008: renders cleanly without overflow on 800x360 landscape phone', (tester) async { tester.view.physicalSize = const Size(800, 360); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await open(tester); expect(tester.takeException(), isNull); expect(find.text('REPORT ROAD INCIDENT'), findsOneWidget); expect(find.text('Police'), findsOneWidget); expect(find.text('Road Hazard'), findsOneWidget); expect(find.text('Weather Hazard'), findsOneWidget); }); }