import 'package:afterwork/data/datasources/event_remote_data_source.dart'; import 'package:afterwork/data/models/event_model.dart'; import 'package:afterwork/presentation/state_management/event_bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; // Mock classes class MockEventRemoteDataSource extends Mock implements EventRemoteDataSource {} void main() { late EventBloc eventBloc; late MockEventRemoteDataSource mockRemoteDataSource; setUpAll(() { // Enregistrer fallback value pour EventModel registerFallbackValue( EventModel( id: 'fallback', title: 'Fallback', description: 'Fallback', startDate: '2026-01-01T00:00:00Z', location: 'Fallback', category: 'Fallback', link: 'Fallback', creatorEmail: 'fallback@example.com', creatorFirstName: 'Fallback', creatorLastName: 'Fallback', profileImageUrl: 'fallback.jpg', participants: const [], status: 'ouvert', reactionsCount: 0, commentsCount: 0, sharesCount: 0, ), ); }); setUp(() { mockRemoteDataSource = MockEventRemoteDataSource(); eventBloc = EventBloc(remoteDataSource: mockRemoteDataSource); }); tearDown(() { eventBloc.close(); }); final tEventModels = [ EventModel( id: '1', title: 'Event 1', description: 'Description 1', startDate: '2026-01-15T19:00:00Z', location: 'Location 1', category: 'Category 1', link: 'https://link1.com', creatorEmail: 'creator1@example.com', creatorFirstName: 'John', creatorLastName: 'Doe', profileImageUrl: 'profile1.jpg', participants: const [], status: 'ouvert', reactionsCount: 0, commentsCount: 0, sharesCount: 0, ), EventModel( id: '2', title: 'Event 2', description: 'Description 2', startDate: '2026-01-16T19:00:00Z', location: 'Location 2', category: 'Category 2', link: 'https://link2.com', creatorEmail: 'creator2@example.com', creatorFirstName: 'Jane', creatorLastName: 'Smith', profileImageUrl: 'profile2.jpg', participants: const [], status: 'ouvert', reactionsCount: 0, commentsCount: 0, sharesCount: 0, ), ]; group('EventBloc', () { test('initial state should be EventInitial', () { // Assert expect(eventBloc.state, isA()); }); group('LoadEvents', () { blocTest( 'should emit [EventLoading, EventLoaded] when data is gotten successfully', build: () { when( () => mockRemoteDataSource .getEventsCreatedByUserAndFriends(any()), ).thenAnswer((_) async => tEventModels); return eventBloc; }, act: (bloc) => bloc.add(const LoadEvents('user123')), expect: () => [ isA(), isA().having((s) => s.events, 'events', tEventModels), ], verify: (_) { verify( () => mockRemoteDataSource .getEventsCreatedByUserAndFriends('user123'), ).called(1); }, ); blocTest( 'should emit [EventLoading, EventError] when getting data fails', build: () { when( () => mockRemoteDataSource .getEventsCreatedByUserAndFriends(any()), ).thenThrow(Exception('Failed to load events')); return eventBloc; }, act: (bloc) => bloc.add(const LoadEvents('user123')), expect: () => [ isA(), isA() .having((s) => s.message, 'message', 'Erreur lors du chargement des événements.'), ], ); blocTest( 'should emit empty list when no events found', build: () { when( () => mockRemoteDataSource .getEventsCreatedByUserAndFriends(any()), ).thenAnswer((_) async => []); return eventBloc; }, act: (bloc) => bloc.add(const LoadEvents('user123')), expect: () => [ isA(), isA().having((s) => s.events, 'events', isEmpty), ], ); }); group('AddEvent', () { final tNewEvent = EventModel( id: '3', title: 'New Event', description: 'New Description', startDate: '2026-01-17T19:00:00Z', location: 'New Location', category: 'New Category', link: 'https://newlink.com', creatorEmail: 'new@example.com', creatorFirstName: 'New', creatorLastName: 'Creator', profileImageUrl: 'newprofile.jpg', participants: const [], status: 'ouvert', reactionsCount: 0, commentsCount: 0, sharesCount: 0, ); blocTest( 'should emit [EventLoading, EventLoaded] when event is added successfully', build: () { when(() => mockRemoteDataSource.createEvent(any())) .thenAnswer((_) async => tNewEvent); when(() => mockRemoteDataSource.getAllEvents()) .thenAnswer((_) async => [...tEventModels, tNewEvent]); return eventBloc; }, act: (bloc) => bloc.add(AddEvent(tNewEvent)), expect: () => [ isA(), isA() .having((s) => s.events.length, 'events.length', 3) .having((s) => s.events.last.id, 'last event id', '3'), ], ); blocTest( 'should emit [EventLoading, EventError] when adding event fails', build: () { when(() => mockRemoteDataSource.createEvent(any())) .thenThrow(Exception('Failed to create event')); return eventBloc; }, act: (bloc) => bloc.add(AddEvent(tNewEvent)), expect: () => [ isA(), isA() .having((s) => s.message, 'message', "Erreur lors de l'ajout de l'événement."), ], ); }); group('CloseEvent', () { blocTest( 'should emit [EventLoading, EventLoaded] with updated event status', build: () { when(() => mockRemoteDataSource.closeEvent(any())) .thenAnswer((_) async => {}); return eventBloc; }, seed: () => EventLoaded(tEventModels), act: (bloc) => bloc.add(const CloseEvent('1')), wait: const Duration(milliseconds: 100), expect: () => [ isA(), predicate((state) { if (state is EventLoaded) { try { final event = state.events.firstWhere((e) => e.id == '1'); return event.status == 'fermé'; } catch (_) { return false; } } return false; }), ], ); blocTest( 'should emit [EventLoading, EventError] when closing event fails', build: () { when(() => mockRemoteDataSource.closeEvent(any())) .thenThrow(Exception('Failed to close event')); return eventBloc; }, seed: () => EventLoaded(tEventModels), act: (bloc) => bloc.add(const CloseEvent('1')), expect: () => [ isA(), isA() .having((s) => s.message, 'message', "Erreur lors de la fermeture de l'événement."), ], ); blocTest( 'should not update state when event is not in loaded list', build: () { when(() => mockRemoteDataSource.closeEvent(any())) .thenAnswer((_) async => {}); return eventBloc; }, seed: () => EventLoaded(tEventModels), act: (bloc) => bloc.add(const CloseEvent('non-existent-id')), expect: () => [ isA(), isA(), // Restaure l'état précédent ], ); }); group('ReopenEvent', () { final closedEvents = tEventModels.map((e) { final event = EventModel( id: e.id, title: e.title, description: e.description, startDate: e.startDate, location: e.location, category: e.category, link: e.link, creatorEmail: e.creatorEmail, creatorFirstName: e.creatorFirstName, creatorLastName: e.creatorLastName, profileImageUrl: e.profileImageUrl, participants: e.participants, status: 'fermé', reactionsCount: e.reactionsCount, commentsCount: e.commentsCount, sharesCount: e.sharesCount, ); return event; }).toList(); blocTest( 'should emit [EventLoading, EventLoaded] with reopened event', build: () { when(() => mockRemoteDataSource.reopenEvent(any())) .thenAnswer((_) async => {}); return eventBloc; }, seed: () => EventLoaded(closedEvents), act: (bloc) => bloc.add(const ReopenEvent('1')), wait: const Duration(milliseconds: 100), expect: () => [ isA(), predicate((state) { if (state is EventLoaded) { try { final event = state.events.firstWhere((e) => e.id == '1'); return event.status == 'ouvert'; } catch (_) { return false; } } return false; }), ], ); blocTest( 'should emit [EventLoading, EventError] when reopening event fails', build: () { when(() => mockRemoteDataSource.reopenEvent(any())) .thenThrow(Exception('Failed to reopen event')); return eventBloc; }, seed: () => EventLoaded(closedEvents), act: (bloc) => bloc.add(const ReopenEvent('1')), expect: () => [ isA(), isA() .having((s) => s.message, 'message', "Erreur lors de la réouverture de l'événement."), ], ); blocTest( 'should not update state when reopening non-existent event', build: () { when(() => mockRemoteDataSource.reopenEvent(any())) .thenAnswer((_) async => {}); return eventBloc; }, seed: () => EventLoaded(closedEvents), act: (bloc) => bloc.add(const ReopenEvent('non-existent-id')), expect: () => [ isA(), isA(), // Restaure l'état précédent ], ); }); group('RemoveEvent', () { blocTest( 'should remove event from list locally', build: () => eventBloc, seed: () => EventLoaded(tEventModels), act: (bloc) => bloc.add(const RemoveEvent('1')), skip: 0, expect: () => [ isA().having( (s) => s.events.length, 'events.length', 1, ).having( (s) => s.events.first.id, 'first event id', '2', ), ], ); blocTest( 'should not emit anything when state is not EventLoaded', build: () => eventBloc, seed: () => const EventInitial(), act: (bloc) => bloc.add(const RemoveEvent('1')), expect: () => [], ); test('should handle removing non-existent event gracefully', () async { // Arrange final bloc = eventBloc; final initialEvents = tEventModels; // Act bloc.emit(EventLoaded(initialEvents)); bloc.add(const RemoveEvent('non-existent-id')); // Wait for the event to be processed await Future.delayed(const Duration(milliseconds: 100)); // Assert final currentState = bloc.state; expect(currentState, isA()); if (currentState is EventLoaded) { expect(currentState.events.length, 2); // La liste reste inchangée } }); }); }); }