/// Tests unitaires pour SolidarityBloc
library solidarity_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/solidarity/bloc/solidarity_bloc.dart';
import 'package:unionflow_mobile_apps/features/solidarity/data/models/demande_aide_model.dart';
import 'package:unionflow_mobile_apps/features/solidarity/data/repositories/demande_aide_repository.dart';

@GenerateMocks([DemandeAideRepository])
import 'solidarity_bloc_test.mocks.dart';

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

DemandeAideModel _makeDemande({
  String id = 'd1',
  String statut = 'EN_ATTENTE',
  String type = 'MEDICAL',
}) =>
    DemandeAideModel(
      id: id,
      titre: 'Aide médicale',
      description: 'Opération chirurgicale urgente',
      type: type,
      statut: statut,
      montantDemande: 500000,
      demandeurId: 'membre1',
      demandeur: 'Koné Awa',
    );

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

void main() {
  late MockDemandeAideRepository mockRepository;

  SolidarityBloc buildBloc() => SolidarityBloc(mockRepository);

  setUp(() {
    mockRepository = MockDemandeAideRepository();
  });

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

  test('initial state has status initial and empty demandes list', () {
    final bloc = buildBloc();
    expect(bloc.state.status, SolidarityStatus.initial);
    expect(bloc.state.demandes, isEmpty);
    bloc.close();
  });

  // ---- LoadDemandesAide ----------------------------------------------------

  group('LoadDemandesAide', () {
    final demandeList = [_makeDemande(), _makeDemande(id: 'd2')];

    blocTest<SolidarityBloc, SolidarityState>(
      'emits loading then loaded with demandes',
      build: () {
        when(mockRepository.getMesDemandes(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => demandeList);
        return buildBloc();
      },
      act: (b) => b.add(const LoadDemandesAide()),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded)
            .having((s) => s.demandes.length, 'count', 2),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'emits loading then loaded with empty list',
      build: () {
        when(mockRepository.getMesDemandes(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => []);
        return buildBloc();
      },
      act: (b) => b.add(const LoadDemandesAide()),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded)
            .having((s) => s.demandes, 'demandes', isEmpty),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'emits loading then error on exception',
      build: () {
        when(mockRepository.getMesDemandes(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenThrow(Exception('network error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadDemandesAide()),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.error)
            .having((s) => s.error, 'error', isNotNull),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'uses custom page and size',
      build: () {
        when(mockRepository.getMesDemandes(page: 1, size: 5))
            .thenAnswer((_) async => [_makeDemande()]);
        return buildBloc();
      },
      act: (b) => b.add(const LoadDemandesAide(page: 1, size: 5)),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded),
      ],
      verify: (_) =>
          verify(mockRepository.getMesDemandes(page: 1, size: 5)),
    );
  });

  // ---- LoadDemandeAideById -------------------------------------------------

  group('LoadDemandeAideById', () {
    final demande = _makeDemande();

    blocTest<SolidarityBloc, SolidarityState>(
      'emits loading then loaded with demandeDetail',
      build: () {
        when(mockRepository.getById(any)).thenAnswer((_) async => demande);
        return buildBloc();
      },
      act: (b) => b.add(LoadDemandeAideById('d1')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded)
            .having((s) => s.demandeDetail?.id, 'detail id', 'd1'),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'emits error when not found',
      build: () {
        when(mockRepository.getById(any))
            .thenThrow(Exception('not found'));
        return buildBloc();
      },
      act: (b) => b.add(LoadDemandeAideById('missing')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.error),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'emits null when repository returns null',
      build: () {
        when(mockRepository.getById(any)).thenAnswer((_) async => null);
        return buildBloc();
      },
      act: (b) => b.add(LoadDemandeAideById('d1')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded)
            .having((s) => s.demandeDetail, 'detail', isNull),
      ],
    );
  });

  // ---- SearchDemandesAide --------------------------------------------------

  group('SearchDemandesAide', () {
    final results = [_makeDemande(type: 'MEDICAL')];

    blocTest<SolidarityBloc, SolidarityState>(
      'emits loading then loaded with filtered results',
      build: () {
        when(mockRepository.search(
          statut: anyNamed('statut'),
          type: anyNamed('type'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        )).thenAnswer((_) async => results);
        return buildBloc();
      },
      act: (b) => b.add(const SearchDemandesAide(
        statut: 'EN_ATTENTE',
        type: 'MEDICAL',
      )),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded)
            .having((s) => s.demandes.length, 'count', 1),
      ],
      verify: (_) => verify(mockRepository.search(
        statut: 'EN_ATTENTE',
        type: 'MEDICAL',
        page: anyNamed('page'),
        size: anyNamed('size'),
      )),
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'emits loading then error on failure',
      build: () {
        when(mockRepository.search(
          statut: anyNamed('statut'),
          type: anyNamed('type'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        )).thenThrow(Exception('search failed'));
        return buildBloc();
      },
      act: (b) => b.add(const SearchDemandesAide(statut: 'APPROUVEE')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.error),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'searches without filters (null statut and type)',
      build: () {
        when(mockRepository.search(
          statut: anyNamed('statut'),
          type: anyNamed('type'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        )).thenAnswer((_) async => results);
        return buildBloc();
      },
      act: (b) => b.add(const SearchDemandesAide()),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded),
      ],
    );
  });

  // ---- CreateDemandeAide ---------------------------------------------------

  group('CreateDemandeAide', () {
    final demande = _makeDemande();

    blocTest<SolidarityBloc, SolidarityState>(
      'creates demande then re-triggers LoadDemandesAide',
      build: () {
        when(mockRepository.create(any)).thenAnswer((_) async => demande);
        when(mockRepository.getMesDemandes(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => [demande]);
        return buildBloc();
      },
      act: (b) => b.add(CreateDemandeAide(demande)),
      expect: () => [
        // Loading from CreateDemandeAide
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        // Loading from auto-triggered LoadDemandesAide
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        // Loaded from LoadDemandesAide
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'emits error on creation failure',
      build: () {
        when(mockRepository.create(any))
            .thenThrow(Exception('creation failed'));
        return buildBloc();
      },
      act: (b) => b.add(CreateDemandeAide(demande)),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.error),
      ],
    );
  });

  // ---- ApprouverDemandeAide ------------------------------------------------

  group('ApprouverDemandeAide', () {
    final approved = _makeDemande(statut: 'APPROUVEE');

    blocTest<SolidarityBloc, SolidarityState>(
      'approves demande and updates demandeDetail, re-triggers LoadDemandesAide',
      build: () {
        when(mockRepository.approuver(any))
            .thenAnswer((_) async => approved);
        when(mockRepository.getMesDemandes(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => [approved]);
        return buildBloc();
      },
      act: (b) => b.add(ApprouverDemandeAide('d1')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded)
            .having((s) => s.demandeDetail?.statut, 'statut', 'APPROUVEE'),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'emits error on failure',
      build: () {
        when(mockRepository.approuver(any))
            .thenThrow(Exception('approval failed'));
        return buildBloc();
      },
      act: (b) => b.add(ApprouverDemandeAide('d1')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.error),
      ],
    );
  });

  // ---- RejeterDemandeAide --------------------------------------------------

  group('RejeterDemandeAide', () {
    final rejected = _makeDemande(statut: 'REJETEE');

    blocTest<SolidarityBloc, SolidarityState>(
      'rejects demande with motif and re-triggers LoadDemandesAide',
      build: () {
        when(mockRepository.rejeter(any, motif: anyNamed('motif')))
            .thenAnswer((_) async => rejected);
        when(mockRepository.getMesDemandes(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => []);
        return buildBloc();
      },
      act: (b) => b.add(RejeterDemandeAide('d1', motif: 'Dossier incomplet')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded)
            .having((s) => s.demandeDetail?.statut, 'statut', 'REJETEE'),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'rejects without motif',
      build: () {
        when(mockRepository.rejeter(any, motif: anyNamed('motif')))
            .thenAnswer((_) async => rejected);
        when(mockRepository.getMesDemandes(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => []);
        return buildBloc();
      },
      act: (b) => b.add(RejeterDemandeAide('d1')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loaded),
      ],
    );

    blocTest<SolidarityBloc, SolidarityState>(
      'emits error on failure',
      build: () {
        when(mockRepository.rejeter(any, motif: anyNamed('motif')))
            .thenThrow(Exception('rejection failed'));
        return buildBloc();
      },
      act: (b) => b.add(RejeterDemandeAide('d1', motif: 'motif')),
      expect: () => [
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.loading),
        isA<SolidarityState>()
            .having((s) => s.status, 'status', SolidarityStatus.error)
            .having((s) => s.message, 'message', isNotNull),
      ],
    );
  });

  // ---- copyWith / state preservation ---------------------------------------

  group('State preservation via copyWith', () {
    test('loaded state preserves existing demandes list on error', () async {
      final existingList = [_makeDemande(id: 'existing')];
      when(mockRepository.getMesDemandes(
              page: anyNamed('page'), size: anyNamed('size')))
          .thenAnswer((_) async => existingList);

      final bloc = buildBloc();
      bloc.add(const LoadDemandesAide());
      await Future.delayed(const Duration(milliseconds: 50));

      expect(bloc.state.status, SolidarityStatus.loaded);
      expect(bloc.state.demandes.length, 1);

      when(mockRepository.getById(any))
          .thenThrow(Exception('lookup failed'));
      bloc.add(LoadDemandeAideById('missing'));
      await Future.delayed(const Duration(milliseconds: 50));

      // demandes list should still be preserved
      expect(bloc.state.demandes.length, 1);
      bloc.close();
    });
  });
}
