/// Tests unitaires pour ContributionsBloc
library contributions_bloc_test;

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/contributions/bloc/contributions_bloc.dart';
import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_event.dart';
import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_state.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contributions.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_by_id.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/create_contribution.dart'
    as uc;
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/update_contribution.dart'
    as uc;
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/delete_contribution.dart'
    as uc;
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/pay_contribution.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_stats.dart';

@GenerateMocks([
  GetContributions,
  GetContributionById,
  uc.CreateContribution,
  uc.UpdateContribution,
  uc.DeleteContribution,
  PayContribution,
  GetContributionStats,
  IContributionRepository,
])
import 'contributions_bloc_test.mocks.dart';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

ContributionModel _makeContribution({
  String id = 'c1',
  ContributionStatus statut = ContributionStatus.payee,
}) =>
    ContributionModel(
      id: id,
      membreId: 'membre1',
      montant: 5000,
      dateEcheance: DateTime(2025, 12, 31),
      annee: 2025,
      statut: statut,
    );

ContributionPageResult _makePageResult(List<ContributionModel> items) =>
    ContributionPageResult(
      contributions: items,
      total: items.length,
      page: 0,
      size: 20,
      totalPages: items.isEmpty ? 0 : 1,
    );

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

void main() {
  late MockGetContributions mockGetContributions;
  late MockGetContributionById mockGetContributionById;
  late MockCreateContribution mockCreateContribution;
  late MockUpdateContribution mockUpdateContribution;
  late MockDeleteContribution mockDeleteContribution;
  late MockPayContribution mockPayContribution;
  late MockGetContributionStats mockGetContributionStats;
  late MockIContributionRepository mockRepository;

  ContributionsBloc buildBloc() => ContributionsBloc(
        mockGetContributions,
        mockGetContributionById,
        mockCreateContribution,
        mockUpdateContribution,
        mockDeleteContribution,
        mockPayContribution,
        mockGetContributionStats,
        mockRepository,
      );

  setUp(() {
    mockGetContributions = MockGetContributions();
    mockGetContributionById = MockGetContributionById();
    mockCreateContribution = MockCreateContribution();
    mockUpdateContribution = MockUpdateContribution();
    mockDeleteContribution = MockDeleteContribution();
    mockPayContribution = MockPayContribution();
    mockGetContributionStats = MockGetContributionStats();
    mockRepository = MockIContributionRepository();
  });

  // ---- initial state -------------------------------------------------------

  test('initial state is ContributionsInitial', () {
    final bloc = buildBloc();
    expect(bloc.state, isA<ContributionsInitial>());
    bloc.close();
  });

  // ---- LoadContributions ---------------------------------------------------

  group('LoadContributions', () {
    final contribution = _makeContribution();
    final pageResult = _makePageResult([contribution]);

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Loaded] on success',
      build: () {
        when(mockGetContributions(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => pageResult);
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributions()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsLoaded>()
            .having((s) => s.contributions.length, 'count', 1)
            .having((s) => s.total, 'total', 1),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Loaded] with empty list',
      build: () {
        when(mockGetContributions(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => _makePageResult([]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributions()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsLoaded>()
            .having((s) => s.contributions, 'contributions', isEmpty),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on exception',
      build: () {
        when(mockGetContributions(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenThrow(Exception('network'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributions()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'respects custom page and size parameters',
      build: () {
        final bigResult = _makePageResult(
            List.generate(5, (i) => _makeContribution(id: 'c$i')));
        when(mockGetContributions(page: 2, size: 5))
            .thenAnswer((_) async => bigResult);
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributions(page: 2, size: 5)),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsLoaded>()
            .having((s) => s.contributions.length, 'count', 5),
      ],
    );
  });

  // ---- LoadContributionById ------------------------------------------------

  group('LoadContributionById', () {
    final contribution = _makeContribution();

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, DetailLoaded] on success',
      build: () {
        when(mockGetContributionById.call(any))
            .thenAnswer((_) async => contribution);
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionById(id: 'c1')),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionDetailLoaded>()
            .having((s) => s.contribution.id, 'id', 'c1'),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] when not found',
      build: () {
        when(mockGetContributionById.call(any))
            .thenThrow(Exception('not found'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionById(id: 'missing')),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>()
            .having((s) => s.message, 'message', contains('Contribution non trouvée')),
      ],
    );
  });

  // ---- CreateContribution --------------------------------------------------

  group('CreateContribution', () {
    final newContribution = _makeContribution(id: 'new1');

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, ContributionCreated] on success',
      build: () {
        when(mockCreateContribution.call(any))
            .thenAnswer((_) async => newContribution);
        return buildBloc();
      },
      act: (b) => b.add(CreateContribution(contribution: newContribution)),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionCreated>()
            .having((s) => s.contribution.id, 'id', 'new1'),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockCreateContribution.call(any))
            .thenThrow(Exception('validation error'));
        return buildBloc();
      },
      act: (b) => b.add(CreateContribution(contribution: newContribution)),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- UpdateContribution --------------------------------------------------

  group('UpdateContribution', () {
    final updatedContribution = _makeContribution(id: 'c1');

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, ContributionUpdated] on success',
      build: () {
        when(mockUpdateContribution.call(any, any))
            .thenAnswer((_) async => updatedContribution);
        return buildBloc();
      },
      act: (b) => b.add(UpdateContribution(
          id: 'c1', contribution: updatedContribution)),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionUpdated>()
            .having((s) => s.contribution.id, 'id', 'c1'),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockUpdateContribution.call(any, any))
            .thenThrow(Exception('update failed'));
        return buildBloc();
      },
      act: (b) => b.add(UpdateContribution(
          id: 'c1', contribution: updatedContribution)),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- DeleteContribution --------------------------------------------------

  group('DeleteContribution', () {
    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, ContributionDeleted] on success',
      build: () {
        when(mockDeleteContribution.call(any)).thenAnswer((_) async => null);
        return buildBloc();
      },
      act: (b) => b.add(const DeleteContribution(id: 'c1')),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionDeleted>()
            .having((s) => s.id, 'id', 'c1'),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockDeleteContribution.call(any))
            .thenThrow(Exception('delete failed'));
        return buildBloc();
      },
      act: (b) => b.add(const DeleteContribution(id: 'c1')),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- SearchContributions -------------------------------------------------

  group('SearchContributions', () {
    final results = [_makeContribution(id: 'sr1')];
    final pageResult = _makePageResult(results);

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Loaded] on success with filters',
      build: () {
        when(mockRepository.getCotisations(
          page: anyNamed('page'),
          size: anyNamed('size'),
          membreId: anyNamed('membreId'),
          statut: anyNamed('statut'),
          type: anyNamed('type'),
          annee: anyNamed('annee'),
        )).thenAnswer((_) async => pageResult);
        return buildBloc();
      },
      act: (b) => b.add(const SearchContributions(
        membreId: 'membre1',
        statut: ContributionStatus.payee,
        annee: 2025,
      )),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsLoaded>(),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockRepository.getCotisations(
          page: anyNamed('page'),
          size: anyNamed('size'),
          membreId: anyNamed('membreId'),
          statut: anyNamed('statut'),
          type: anyNamed('type'),
          annee: anyNamed('annee'),
        )).thenThrow(Exception('search failed'));
        return buildBloc();
      },
      act: (b) => b.add(const SearchContributions()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- LoadContributionsByMembre -------------------------------------------

  group('LoadContributionsByMembre', () {
    final pageResult = _makePageResult([_makeContribution()]);

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Loaded] on success',
      build: () {
        when(mockRepository.getCotisations(
          page: anyNamed('page'),
          size: anyNamed('size'),
          membreId: anyNamed('membreId'),
          statut: anyNamed('statut'),
          type: anyNamed('type'),
          annee: anyNamed('annee'),
        )).thenAnswer((_) async => pageResult);
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsByMembre(membreId: 'membre1')),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsLoaded>(),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockRepository.getCotisations(
          page: anyNamed('page'),
          size: anyNamed('size'),
          membreId: anyNamed('membreId'),
          statut: anyNamed('statut'),
          type: anyNamed('type'),
          annee: anyNamed('annee'),
        )).thenThrow(Exception('load failed'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsByMembre(membreId: 'membre1')),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- LoadContributionsPayees ---------------------------------------------

  group('LoadContributionsPayees', () {
    final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee);
    final nonPayee =
        _makeContribution(id: 'np1', statut: ContributionStatus.nonPayee);

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Loaded] with only paid contributions',
      build: () {
        when(mockRepository.getMesCotisations())
            .thenAnswer((_) async => _makePageResult([payee, nonPayee]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsPayees()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsLoaded>()
            .having((s) => s.contributions.length, 'count', 1)
            .having((s) => s.contributions.first.id, 'id', 'p1'),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockRepository.getMesCotisations())
            .thenThrow(Exception('server error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsPayees()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- LoadContributionsNonPayees ------------------------------------------

  group('LoadContributionsNonPayees', () {
    final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee);
    final nonPayee =
        _makeContribution(id: 'np1', statut: ContributionStatus.nonPayee);

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Loaded] with only unpaid contributions',
      build: () {
        when(mockRepository.getMesCotisations())
            .thenAnswer((_) async => _makePageResult([payee, nonPayee]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsNonPayees()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsLoaded>()
            .having((s) => s.contributions.length, 'count', 1)
            .having((s) => s.contributions.first.id, 'id', 'np1'),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockRepository.getMesCotisations())
            .thenThrow(Exception('server error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsNonPayees()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- LoadContributionsEnRetard -------------------------------------------

  group('LoadContributionsEnRetard', () {
    final enRetard =
        _makeContribution(id: 'r1', statut: ContributionStatus.enRetard);
    final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee);

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Loaded] with only late contributions',
      build: () {
        when(mockRepository.getMesCotisations())
            .thenAnswer((_) async => _makePageResult([enRetard, payee]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsEnRetard()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsLoaded>()
            .having((s) => s.contributions.length, 'count', 1)
            .having((s) => s.contributions.first.id, 'id', 'r1'),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockRepository.getMesCotisations())
            .thenThrow(Exception('server error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsEnRetard()),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- RecordPayment -------------------------------------------------------

  group('RecordPayment', () {
    final paid = _makeContribution(statut: ContributionStatus.payee);

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, PaymentRecorded] on success',
      build: () {
        when(mockPayContribution.call(
          cotisationId: anyNamed('cotisationId'),
          montant: anyNamed('montant'),
          datePaiement: anyNamed('datePaiement'),
          methodePaiement: anyNamed('methodePaiement'),
          numeroPaiement: anyNamed('numeroPaiement'),
          referencePaiement: anyNamed('referencePaiement'),
        )).thenAnswer((_) async => paid);
        return buildBloc();
      },
      act: (b) => b.add(RecordPayment(
        contributionId: 'c1',
        montant: 5000,
        methodePaiement: PaymentMethod.especes,
        datePaiement: DateTime(2025, 6, 1),
      )),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<PaymentRecorded>(),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockPayContribution.call(
          cotisationId: anyNamed('cotisationId'),
          montant: anyNamed('montant'),
          datePaiement: anyNamed('datePaiement'),
          methodePaiement: anyNamed('methodePaiement'),
          numeroPaiement: anyNamed('numeroPaiement'),
          referencePaiement: anyNamed('referencePaiement'),
        )).thenThrow(Exception('payment failed'));
        return buildBloc();
      },
      act: (b) => b.add(RecordPayment(
        contributionId: 'c1',
        montant: 5000,
        methodePaiement: PaymentMethod.waveMoney,
        datePaiement: DateTime(2025, 6, 1),
      )),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- LoadContributionsStats ----------------------------------------------

  group('LoadContributionsStats', () {
    final synthese = {
      'montantDu': 10000.0,
      'totalPayeAnnee': 5000.0,
      'cotisationsEnAttente': 2,
      'prochaineEcheance': '2025-07-31',
      'anneeEnCours': 2025,
    };

    blocTest<ContributionsBloc, ContributionsState>(
      'emits ContributionsStatsLoaded using synthese when non-null',
      build: () {
        when(mockGetContributionStats.call()).thenAnswer((_) async => synthese);
        // getContributions called when no preserved list
        when(mockGetContributions(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => _makePageResult([]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsStats()),
      expect: () => [
        isA<ContributionsStatsLoaded>()
            .having((s) => s.stats['isMesSynthese'], 'isMesSynthese', true),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'falls back to repository.getStatistiques when synthese is null',
      build: () {
        when(mockGetContributionStats.call()).thenAnswer((_) async => null);
        when(mockGetContributions(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => _makePageResult([]));
        when(mockRepository.getStatistiques())
            .thenAnswer((_) async => {'totalCotisations': 10});
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsStats()),
      expect: () => [
        isA<ContributionsStatsLoaded>(),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits ContributionsError on failure',
      build: () {
        when(mockGetContributionStats.call())
            .thenThrow(Exception('stats failed'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadContributionsStats()),
      expect: () => [isA<ContributionsError>()],
    );
  });

  // ---- GenerateAnnualContributions -----------------------------------------

  group('GenerateAnnualContributions', () {
    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, ContributionsGenerated] on success',
      build: () {
        when(mockRepository.genererCotisationsAnnuelles(any))
            .thenAnswer((_) async => 42);
        return buildBloc();
      },
      act: (b) => b.add(GenerateAnnualContributions(
        annee: 2025,
        montant: 10000,
        dateEcheance: DateTime(2025, 12, 31),
      )),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsGenerated>()
            .having((s) => s.nombreGenere, 'nombreGenere', 42),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockRepository.genererCotisationsAnnuelles(any))
            .thenThrow(Exception('generate failed'));
        return buildBloc();
      },
      act: (b) => b.add(GenerateAnnualContributions(
        annee: 2025,
        montant: 10000,
        dateEcheance: DateTime(2025, 12, 31),
      )),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });

  // ---- SendPaymentReminder -------------------------------------------------

  group('SendPaymentReminder', () {
    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, ReminderSent] on success',
      build: () {
        when(mockRepository.envoyerRappel(any)).thenAnswer((_) async => null);
        return buildBloc();
      },
      act: (b) => b.add(const SendPaymentReminder(contributionId: 'c1')),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ReminderSent>()
            .having((s) => s.contributionId, 'contributionId', 'c1'),
      ],
    );

    blocTest<ContributionsBloc, ContributionsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockRepository.envoyerRappel(any))
            .thenThrow(Exception('reminder failed'));
        return buildBloc();
      },
      act: (b) => b.add(const SendPaymentReminder(contributionId: 'c1')),
      expect: () => [
        isA<ContributionsLoading>(),
        isA<ContributionsError>(),
      ],
    );
  });
}
