import 'package:flutter_test/flutter_test.dart'; import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/chat_settings/utils/bot_client_extension.dart'; import 'package:fluffychat/pangea/learning_settings/gender_enum.dart'; import 'package:fluffychat/pangea/learning_settings/language_level_type_enum.dart'; import 'package:fluffychat/pangea/user/user_model.dart'; void main() { // --------------------------------------------------------------------------- // buildUpdatedBotOptions — pure logic tests // --------------------------------------------------------------------------- group('buildUpdatedBotOptions', () { final baseSettings = UserSettings( targetLanguage: 'es', cefrLevel: LanguageLevelTypeEnum.b1, voice: 'voice_1', gender: GenderEnum.woman, ); const userId = '@user:server'; test('returns null when all relevant fields already match', () { final currentOptions = BotOptionsModel( targetLanguage: 'es', languageLevel: LanguageLevelTypeEnum.b1, targetVoice: 'voice_1', userGenders: const {userId: GenderEnum.woman}, ); final result = buildUpdatedBotOptions( currentOptions: currentOptions, userSettings: baseSettings, userId: userId, ); expect(result, isNull); }); test('returns updated model when targetLanguage differs', () { final currentOptions = BotOptionsModel( targetLanguage: 'fr', languageLevel: LanguageLevelTypeEnum.b1, targetVoice: 'voice_1', userGenders: const {userId: GenderEnum.woman}, ); final result = buildUpdatedBotOptions( currentOptions: currentOptions, userSettings: baseSettings, userId: userId, ); expect(result, isNotNull); expect(result!.targetLanguage, 'es'); // Other fields carried over expect(result.languageLevel, LanguageLevelTypeEnum.b1); expect(result.targetVoice, 'voice_1'); }); test('returns updated model when languageLevel differs', () { final currentOptions = BotOptionsModel( targetLanguage: 'es', languageLevel: LanguageLevelTypeEnum.a1, targetVoice: 'voice_1', userGenders: const {userId: GenderEnum.woman}, ); final result = buildUpdatedBotOptions( currentOptions: currentOptions, userSettings: baseSettings, userId: userId, ); expect(result, isNotNull); expect(result!.languageLevel, LanguageLevelTypeEnum.b1); }); test('returns updated model when voice differs', () { final currentOptions = BotOptionsModel( targetLanguage: 'es', languageLevel: LanguageLevelTypeEnum.b1, targetVoice: 'voice_2', userGenders: const {userId: GenderEnum.woman}, ); final result = buildUpdatedBotOptions( currentOptions: currentOptions, userSettings: baseSettings, userId: userId, ); expect(result, isNotNull); expect(result!.targetVoice, 'voice_1'); }); test('returns updated model when gender differs', () { final currentOptions = BotOptionsModel( targetLanguage: 'es', languageLevel: LanguageLevelTypeEnum.b1, targetVoice: 'voice_1', userGenders: const {userId: GenderEnum.man}, ); final result = buildUpdatedBotOptions( currentOptions: currentOptions, userSettings: baseSettings, userId: userId, ); expect(result, isNotNull); expect(result!.userGenders[userId], GenderEnum.woman); }); test('defaults to empty BotOptionsModel when currentOptions is null', () { final result = buildUpdatedBotOptions( currentOptions: null, userSettings: baseSettings, userId: userId, ); expect(result, isNotNull); expect(result!.targetLanguage, 'es'); expect(result.languageLevel, LanguageLevelTypeEnum.b1); expect(result.targetVoice, 'voice_1'); expect(result.userGenders[userId], GenderEnum.woman); }); test('preserves gender entries for other users', () { final currentOptions = BotOptionsModel( targetLanguage: 'fr', // different → triggers update languageLevel: LanguageLevelTypeEnum.b1, targetVoice: 'voice_1', userGenders: const { '@other:server': GenderEnum.man, userId: GenderEnum.woman, }, ); final result = buildUpdatedBotOptions( currentOptions: currentOptions, userSettings: baseSettings, userId: userId, ); expect(result, isNotNull); expect(result!.userGenders['@other:server'], GenderEnum.man); expect(result.userGenders[userId], GenderEnum.woman); }); test('handles null userId gracefully', () { const currentOptions = BotOptionsModel(targetLanguage: 'fr'); final result = buildUpdatedBotOptions( currentOptions: currentOptions, userSettings: baseSettings, userId: null, ); expect(result, isNotNull); expect(result!.targetLanguage, 'es'); // Gender not set because userId is null expect(result.userGenders, isEmpty); }); }); // --------------------------------------------------------------------------- // applyBotOptionUpdatesInOrder — async orchestration tests // --------------------------------------------------------------------------- group('applyBotOptionUpdatesInOrder', () { test('executes priority update before remaining updates', () async { final callLog = []; await applyBotOptionUpdatesInOrder( priorityUpdate: () async { callLog.add('dm'); }, remainingUpdates: [ () async { callLog.add('room_a'); }, () async { callLog.add('room_b'); }, ], ); expect(callLog, ['dm', 'room_a', 'room_b']); }); test('executes remaining updates sequentially, not in parallel', () async { final timestamps = {}; var counter = 0; await applyBotOptionUpdatesInOrder( priorityUpdate: () async { timestamps['dm_start'] = counter++; await Future.delayed(const Duration(milliseconds: 30)); timestamps['dm_end'] = counter++; }, remainingUpdates: [ () async { timestamps['a_start'] = counter++; await Future.delayed(const Duration(milliseconds: 30)); timestamps['a_end'] = counter++; }, () async { timestamps['b_start'] = counter++; await Future.delayed(const Duration(milliseconds: 30)); timestamps['b_end'] = counter++; }, ], ); // Sequential order: dm completes before a starts, a before b expect(timestamps['dm_end']!, lessThan(timestamps['a_start']!)); expect(timestamps['a_end']!, lessThan(timestamps['b_start']!)); }); test('propagates priority update errors to caller', () async { expect( () => applyBotOptionUpdatesInOrder( priorityUpdate: () async { throw Exception('DM update failed'); }, remainingUpdates: [], ), throwsA(isA()), ); }); test( 'remaining updates do NOT execute when priority update fails', () async { final callLog = []; try { await applyBotOptionUpdatesInOrder( priorityUpdate: () async { throw Exception('DM failed'); }, remainingUpdates: [ () async { callLog.add('room_a'); }, ], ); } catch (_) {} expect(callLog, isEmpty); }, ); test( 'isolates errors in remaining updates and continues to next room', () async { final callLog = []; final errors = []; await applyBotOptionUpdatesInOrder( priorityUpdate: () async { callLog.add('dm'); }, remainingUpdates: [ () async { callLog.add('room_a'); }, () async { throw Exception('room_b failed'); }, () async { callLog.add('room_c'); }, ], onError: (e, _) => errors.add(e), ); // room_b's error didn't prevent room_c from running expect(callLog, ['dm', 'room_a', 'room_c']); expect(errors, hasLength(1)); expect(errors.first, isA()); }, ); test('works correctly when priority update is null', () async { final callLog = []; await applyBotOptionUpdatesInOrder( priorityUpdate: null, remainingUpdates: [ () async { callLog.add('room_a'); }, () async { callLog.add('room_b'); }, ], ); expect(callLog, ['room_a', 'room_b']); }); test('handles empty remaining updates list', () async { final callLog = []; await applyBotOptionUpdatesInOrder( priorityUpdate: () async { callLog.add('dm'); }, remainingUpdates: [], ); expect(callLog, ['dm']); }); test('handles all null / empty gracefully', () async { // Should complete without error await applyBotOptionUpdatesInOrder( priorityUpdate: null, remainingUpdates: [], ); }); test('multiple remaining errors are all reported', () async { final errors = []; await applyBotOptionUpdatesInOrder( priorityUpdate: null, remainingUpdates: [ () async { throw Exception('fail_1'); }, () async { throw Exception('fail_2'); }, () async { throw Exception('fail_3'); }, ], onError: (e, _) => errors.add(e), ); expect(errors, hasLength(3)); }); }); }