import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:unionflow_mobile_apps/features/reports/presentation/bloc/reports_bloc.dart'; import 'package:unionflow_mobile_apps/features/reports/domain/usecases/generate_report.dart'; import 'package:unionflow_mobile_apps/features/reports/domain/usecases/schedule_report.dart'; import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart'; @GenerateMocks([GenerateReport, ScheduleReport, IReportsRepository]) import 'reports_bloc_test.mocks.dart'; void main() { late ReportsBloc bloc; late MockGenerateReport mockGenerateReport; late MockScheduleReport mockScheduleReport; late MockIReportsRepository mockRepository; // ── Fixtures ────────────────────────────────────────────────────────────── Map fakePerformance() => {'totalMembres': 150, 'cotisationsCollectees': 1500000.0}; Map fakeStatsMembres() => {'totalMembres': 150, 'membresActifs': 120}; Map fakeStatsCotisations() => {'totalCotisations': 200, 'totalMontant': 1500000.0}; Map fakeStatsEvenements() => {'totalEvenements': 12, 'participantsTotal': 300}; setUp(() { mockGenerateReport = MockGenerateReport(); mockScheduleReport = MockScheduleReport(); mockRepository = MockIReportsRepository(); bloc = ReportsBloc(mockGenerateReport, mockScheduleReport, mockRepository); }); tearDown(() => bloc.close()); // ── Initial state ───────────────────────────────────────────────────────── test('initial state is ReportsInitial', () { expect(bloc.state, isA()); }); // ── LoadDashboardReports ────────────────────────────────────────────────── group('LoadDashboardReports', () { void stubDashboardSuccess() { when(mockRepository.getPerformanceGlobale()) .thenAnswer((_) async => fakePerformance()); when(mockRepository.getStatistiquesMembres()) .thenAnswer((_) async => fakeStatsMembres()); when(mockRepository.getStatistiquesCotisations(any)) .thenAnswer((_) async => fakeStatsCotisations()); when(mockRepository.getStatistiquesEvenements()) .thenAnswer((_) async => fakeStatsEvenements()); } blocTest( 'emits [ReportsLoading, ReportsDashboardLoaded] on success', build: () { stubDashboardSuccess(); return bloc; }, act: (b) => b.add(const LoadDashboardReports()), expect: () => [ isA(), isA() .having( (s) => s.performance['totalMembres'], 'performance.totalMembres', 150, ) .having( (s) => s.statsMembres['membresActifs'], 'statsMembres.membresActifs', 120, ) .having( (s) => s.statsCotisations['totalCotisations'], 'statsCotisations.totalCotisations', 200, ) .having( (s) => s.statsEvenements['totalEvenements'], 'statsEvenements.totalEvenements', 12, ), ], verify: (_) { verify(mockRepository.getPerformanceGlobale()).called(1); verify(mockRepository.getStatistiquesMembres()).called(1); verify(mockRepository.getStatistiquesCotisations(any)).called(1); verify(mockRepository.getStatistiquesEvenements()).called(1); }, ); blocTest( 'emits [ReportsLoading, ReportsError] when performance call fails', build: () { when(mockRepository.getPerformanceGlobale()) .thenThrow(Exception('network error')); when(mockRepository.getStatistiquesMembres()) .thenAnswer((_) async => fakeStatsMembres()); when(mockRepository.getStatistiquesCotisations(any)) .thenAnswer((_) async => fakeStatsCotisations()); when(mockRepository.getStatistiquesEvenements()) .thenAnswer((_) async => fakeStatsEvenements()); return bloc; }, act: (b) => b.add(const LoadDashboardReports()), expect: () => [ isA(), isA().having( (s) => s.message, 'message', contains('Erreur'), ), ], ); blocTest( 'emits [ReportsLoading, ReportsError] when stats membres call fails', build: () { when(mockRepository.getPerformanceGlobale()) .thenAnswer((_) async => fakePerformance()); when(mockRepository.getStatistiquesMembres()) .thenThrow(Exception('membres error')); when(mockRepository.getStatistiquesCotisations(any)) .thenAnswer((_) async => fakeStatsCotisations()); when(mockRepository.getStatistiquesEvenements()) .thenAnswer((_) async => fakeStatsEvenements()); return bloc; }, act: (b) => b.add(const LoadDashboardReports()), expect: () => [isA(), isA()], ); blocTest( 'passes current year to getStatistiquesCotisations', build: () { stubDashboardSuccess(); return bloc; }, act: (b) => b.add(const LoadDashboardReports()), verify: (_) { final captured = verify( mockRepository.getStatistiquesCotisations(captureAny), ).captured; expect(captured.first, equals(DateTime.now().year)); }, ); }); // ── ScheduleReportRequested ─────────────────────────────────────────────── group('ScheduleReportRequested', () { blocTest( 'emits [ReportScheduled] on schedule success', build: () { when(mockScheduleReport(cronExpression: anyNamed('cronExpression'))) .thenAnswer((_) async {}); return bloc; }, act: (b) => b.add(const ScheduleReportRequested(cronExpression: '0 0 1 * *')), expect: () => [isA()], verify: (_) { verify(mockScheduleReport(cronExpression: '0 0 1 * *')).called(1); }, ); blocTest( 'emits [ReportScheduled] on schedule without cron expression', build: () { when(mockScheduleReport(cronExpression: anyNamed('cronExpression'))) .thenAnswer((_) async {}); return bloc; }, act: (b) => b.add(const ScheduleReportRequested()), expect: () => [isA()], ); blocTest( 'emits [ReportsError] on schedule failure', build: () { when(mockScheduleReport(cronExpression: anyNamed('cronExpression'))) .thenThrow(Exception('schedule error')); return bloc; }, act: (b) => b.add(const ScheduleReportRequested()), expect: () => [ isA().having( (s) => s.message, 'message', contains('Impossible de programmer'), ), ], ); }); // ── GenerateReportRequested ─────────────────────────────────────────────── group('GenerateReportRequested', () { blocTest( 'emits [ReportGenerated] on generate success without format', build: () { when(mockGenerateReport('membres', format: anyNamed('format'))) .thenAnswer((_) async {}); return bloc; }, act: (b) => b.add(const GenerateReportRequested('membres')), expect: () => [ isA().having( (s) => s.type, 'type', 'membres', ), ], verify: (_) { verify(mockGenerateReport('membres', format: null)).called(1); }, ); blocTest( 'emits [ReportGenerated] on generate success with pdf format', build: () { when(mockGenerateReport('cotisations', format: 'pdf')) .thenAnswer((_) async {}); return bloc; }, act: (b) => b.add(const GenerateReportRequested('cotisations', format: 'pdf')), expect: () => [ isA().having((s) => s.type, 'type', 'cotisations'), ], verify: (_) { verify(mockGenerateReport('cotisations', format: 'pdf')).called(1); }, ); blocTest( 'emits [ReportsError] on generate failure', build: () { when(mockGenerateReport(any, format: anyNamed('format'))) .thenThrow(Exception('generate error')); return bloc; }, act: (b) => b.add(const GenerateReportRequested('evenements')), expect: () => [ isA().having( (s) => s.message, 'message', contains('Impossible de générer'), ), ], ); blocTest( 'emits [ReportsError] for excel format failure', build: () { when(mockGenerateReport(any, format: 'excel')).thenThrow(Exception('excel error')); return bloc; }, act: (b) => b.add(const GenerateReportRequested('finance', format: 'excel')), expect: () => [isA()], ); }); }