import 'dart:convert'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:social_app/core/notifications/local_notification_service.dart'; import 'package:social_app/core/notifications/reminder_notification_callbacks.dart'; import 'package:social_app/features/calendar/reminders/models/reminder_action.dart'; import 'package:social_app/features/calendar/reminders/models/reminder_payload.dart'; class MockFlutterLocalNotificationsPlugin extends Mock implements FlutterLocalNotificationsPlugin {} void main() { setUpAll(() { registerFallbackValue( const InitializationSettings( android: AndroidInitializationSettings('@mipmap/ic_launcher'), iOS: DarwinInitializationSettings(), ), ); }); late MockFlutterLocalNotificationsPlugin plugin; late LocalNotificationService service; late List handledActions; late List presentedPayloads; DidReceiveNotificationResponseCallback? callback; setUp(() async { SharedPreferences.setMockInitialValues({}); plugin = MockFlutterLocalNotificationsPlugin(); service = LocalNotificationService(plugin: plugin); handledActions = []; presentedPayloads = []; when( () => plugin.initialize( any(), onDidReceiveNotificationResponse: any( named: 'onDidReceiveNotificationResponse', ), onDidReceiveBackgroundNotificationResponse: any( named: 'onDidReceiveBackgroundNotificationResponse', ), ), ).thenAnswer((invocation) async { callback = invocation.namedArguments[#onDidReceiveNotificationResponse] as DidReceiveNotificationResponseCallback?; return true; }); when( () => plugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin >(), ).thenReturn(null); when( () => plugin .resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin >(), ).thenReturn(null); service.bindActionHandler(({required action, required payload}) async { handledActions.add(action); }); service.bindInAppReminderHandler((payload) async { presentedPayloads.add(payload); }); await ReminderNotificationCallbacks.bindResponseHandler( service.handleNotificationResponse, ); await service.initialize(); }); test('cancel action from system notification maps to archive', () async { callback!( NotificationResponse( notificationResponseType: NotificationResponseType.selectedNotificationAction, id: 101, actionId: 'cancel', payload: jsonEncode( ReminderPayload( eventId: 'evt_1', title: 'sync', startAt: DateTime.parse('2026-03-19T10:00:00+08:00'), timezone: 'Asia/Shanghai', ).toJson(), ), ), ); await Future.delayed(Duration.zero); expect(handledActions, [ReminderAction.archive]); }); test('duplicate notification response is handled only once', () async { final response = NotificationResponse( notificationResponseType: NotificationResponseType.selectedNotificationAction, id: 201, actionId: 'cancel', payload: jsonEncode( ReminderPayload( eventId: 'evt_2', title: 'retro', startAt: DateTime.parse('2026-03-19T11:00:00+08:00'), timezone: 'Asia/Shanghai', ).toJson(), ), ); callback!(response); callback!(response); await Future.delayed(Duration.zero); expect(handledActions, [ReminderAction.archive]); }); test('notification body tap forwards payload to in-app presenter', () async { callback!( NotificationResponse( notificationResponseType: NotificationResponseType.selectedNotification, id: 301, payload: jsonEncode( ReminderPayload( eventId: 'evt_3', title: 'daily sync', startAt: DateTime.parse('2026-03-19T12:00:00+08:00'), timezone: 'Asia/Shanghai', ).toJson(), ), ), ); await Future.delayed(Duration.zero); expect(presentedPayloads.map((item) => item.eventId), ['evt_3']); expect(handledActions, isEmpty); }); }