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<String, dynamic> fakePerformance() =>
      {'totalMembres': 150, 'cotisationsCollectees': 1500000.0};
  Map<String, dynamic> fakeStatsMembres() =>
      {'totalMembres': 150, 'membresActifs': 120};
  Map<String, dynamic> fakeStatsCotisations() =>
      {'totalCotisations': 200, 'totalMontant': 1500000.0};
  Map<String, dynamic> 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<ReportsInitial>());
  });

  // ── 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<ReportsBloc, ReportsState>(
      'emits [ReportsLoading, ReportsDashboardLoaded] on success',
      build: () {
        stubDashboardSuccess();
        return bloc;
      },
      act: (b) => b.add(const LoadDashboardReports()),
      expect: () => [
        isA<ReportsLoading>(),
        isA<ReportsDashboardLoaded>()
            .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<ReportsBloc, ReportsState>(
      '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<ReportsLoading>(),
        isA<ReportsError>().having(
          (s) => s.message,
          'message',
          contains('Erreur'),
        ),
      ],
    );

    blocTest<ReportsBloc, ReportsState>(
      '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<ReportsLoading>(), isA<ReportsError>()],
    );

    blocTest<ReportsBloc, ReportsState>(
      '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<ReportsBloc, ReportsState>(
      '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<ReportScheduled>()],
      verify: (_) {
        verify(mockScheduleReport(cronExpression: '0 0 1 * *')).called(1);
      },
    );

    blocTest<ReportsBloc, ReportsState>(
      '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<ReportScheduled>()],
    );

    blocTest<ReportsBloc, ReportsState>(
      '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<ReportsError>().having(
          (s) => s.message,
          'message',
          contains('Impossible de programmer'),
        ),
      ],
    );
  });

  // ── GenerateReportRequested ───────────────────────────────────────────────

  group('GenerateReportRequested', () {
    blocTest<ReportsBloc, ReportsState>(
      '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<ReportGenerated>().having(
          (s) => s.type,
          'type',
          'membres',
        ),
      ],
      verify: (_) {
        verify(mockGenerateReport('membres', format: null)).called(1);
      },
    );

    blocTest<ReportsBloc, ReportsState>(
      '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<ReportGenerated>().having((s) => s.type, 'type', 'cotisations'),
      ],
      verify: (_) {
        verify(mockGenerateReport('cotisations', format: 'pdf')).called(1);
      },
    );

    blocTest<ReportsBloc, ReportsState>(
      '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<ReportsError>().having(
          (s) => s.message,
          'message',
          contains('Impossible de générer'),
        ),
      ],
    );

    blocTest<ReportsBloc, ReportsState>(
      '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<ReportsError>()],
    );
  });
}
