import 'package:bloc_test/bloc_test.dart';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/bloc/organizations_bloc.dart';
import 'package:unionflow_mobile_apps/features/organizations/bloc/organizations_event.dart';
import 'package:unionflow_mobile_apps/features/organizations/bloc/organizations_state.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/services/organization_service.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/create_organization.dart'
    as uc;
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/delete_organization.dart'
    as uc;
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organization_by_id.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organizations.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/update_organization.dart'
    as uc;

@GenerateMocks([
  GetOrganizations,
  GetOrganizationById,
  uc.CreateOrganization,
  uc.UpdateOrganization,
  uc.DeleteOrganization,
  IOrganizationRepository,
  OrganizationService,
])
import 'organizations_bloc_test.mocks.dart';

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

OrganizationModel _makeOrg({
  String id = 'org-1',
  String nom = 'Association Test',
  StatutOrganization statut = StatutOrganization.active,
  String type = 'ASSOCIATION',
  int nombreMembres = 10,
}) =>
    OrganizationModel(
      id: id,
      nom: nom,
      typeOrganisation: type,
      statut: statut,
      nombreMembres: nombreMembres,
    );

// ---------------------------------------------------------------------------

void main() {
  late MockGetOrganizations mockGetOrganizations;
  late MockGetOrganizationById mockGetOrganizationById;
  late MockCreateOrganization mockCreateOrganization;
  late MockUpdateOrganization mockUpdateOrganization;
  late MockDeleteOrganization mockDeleteOrganization;
  late MockIOrganizationRepository mockRepository;
  late MockOrganizationService mockOrganizationService;

  OrganizationsBloc buildBloc() => OrganizationsBloc(
        mockGetOrganizations,
        mockGetOrganizationById,
        mockCreateOrganization,
        mockUpdateOrganization,
        mockDeleteOrganization,
        mockRepository,
        mockOrganizationService,
      );

  setUp(() {
    mockGetOrganizations = MockGetOrganizations();
    mockGetOrganizationById = MockGetOrganizationById();
    mockCreateOrganization = MockCreateOrganization();
    mockUpdateOrganization = MockUpdateOrganization();
    mockDeleteOrganization = MockDeleteOrganization();
    mockRepository = MockIOrganizationRepository();
    mockOrganizationService = MockOrganizationService();

    // Default stubs for searchLocal / filterByStatus / filterByType
    when(mockOrganizationService.searchLocal(any, any)).thenAnswer(
      (invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
    );
    when(mockOrganizationService.filterByStatus(any, any)).thenAnswer(
      (invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
    );
    when(mockOrganizationService.filterByType(any, any)).thenAnswer(
      (invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
    );
    when(mockOrganizationService.sortByName(any, ascending: anyNamed('ascending'))).thenAnswer(
      (invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
    );
    when(mockOrganizationService.sortByCreationDate(any, ascending: anyNamed('ascending'))).thenAnswer(
      (invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
    );
    when(mockOrganizationService.sortByMemberCount(any, ascending: anyNamed('ascending'))).thenAnswer(
      (invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
    );
  });

  // ─── Initial state ────────────────────────────────────────────────────────

  group('initial state', () {
    test('is OrganizationsInitial', () {
      expect(buildBloc().state, const OrganizationsInitial());
    });
  });

  // ─── LoadOrganizations ────────────────────────────────────────────────────

  group('LoadOrganizations', () {
    final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'Mutuelle B')];

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Loading, Loaded] on success',
      build: buildBloc,
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenAnswer((_) async => orgs),
      act: (b) => b.add(const LoadOrganizations()),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsLoaded>()
            .having((s) => s.organizations, 'organizations', orgs)
            .having((s) => s.currentPage, 'page', 0),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Loading, Error] on exception',
      build: buildBloc,
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenThrow(Exception('network error')),
      act: (b) => b.add(const LoadOrganizations()),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsError>()
            .having((e) => e.message, 'message', contains('chargement des organisations')),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'uses useMesOnly endpoint when flag is set',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.getMesOrganisations()).thenAnswer((_) async => orgs),
      act: (b) => b.add(const LoadOrganizations(useMesOnly: true)),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsLoaded>().having((s) => s.useMesOnly, 'useMesOnly', true),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'filters by filterOrganizationIds when provided',
      build: buildBloc,
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenAnswer((_) async => orgs),
      act: (b) => b.add(const LoadOrganizations(filterOrganizationIds: ['org-1'])),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsLoaded>()
            .having((s) => s.organizations.length, 'count', 1)
            .having((s) => s.organizations.first.id, 'id', 'org-1'),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'does NOT emit Loading when state is already Loaded and refresh=false',
      build: buildBloc,
      seed: () => OrganizationsLoaded(
        organizations: orgs,
        filteredOrganizations: orgs,
      ),
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenAnswer((_) async => orgs),
      act: (b) => b.add(const LoadOrganizations()),
      expect: () => [
        isA<OrganizationsLoaded>(),
      ],
    );
  });

  // ─── LoadMoreOrganizations ────────────────────────────────────────────────

  group('LoadMoreOrganizations', () {
    final initialOrgs = [_makeOrg()];
    final moreOrgs = [_makeOrg(id: 'org-2', nom: 'Org B')];
    final loadedState = OrganizationsLoaded(
      organizations: initialOrgs,
      filteredOrganizations: initialOrgs,
      hasReachedMax: false,
      currentPage: 0,
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [LoadingMore, Loaded] with appended orgs',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenAnswer((_) async => moreOrgs),
      act: (b) => b.add(const LoadMoreOrganizations()),
      expect: () => [
        isA<OrganizationsLoadingMore>(),
        isA<OrganizationsLoaded>()
            .having((s) => s.organizations.length, 'total', 2)
            .having((s) => s.currentPage, 'page', 1),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'does nothing when hasReachedMax is true',
      build: buildBloc,
      seed: () => OrganizationsLoaded(
        organizations: initialOrgs,
        filteredOrganizations: initialOrgs,
        hasReachedMax: true,
      ),
      act: (b) => b.add(const LoadMoreOrganizations()),
      expect: () => [],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'does nothing when state is not OrganizationsLoaded',
      build: buildBloc,
      act: (b) => b.add(const LoadMoreOrganizations()),
      expect: () => [],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'sets hasReachedMax when fewer than 20 items returned',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenAnswer((_) async => moreOrgs),
      act: (b) => b.add(const LoadMoreOrganizations()),
      expect: () => [
        isA<OrganizationsLoadingMore>(),
        isA<OrganizationsLoaded>().having((s) => s.hasReachedMax, 'hasReachedMax', true),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits error with previousOrganizations on failure',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenThrow(Exception('network')),
      act: (b) => b.add(const LoadMoreOrganizations()),
      expect: () => [
        isA<OrganizationsLoadingMore>(),
        isA<OrganizationsError>()
            .having((e) => e.previousOrganizations, 'previousOrgs', isNotNull),
      ],
    );
  });

  // ─── SearchOrganizations ──────────────────────────────────────────────────

  group('SearchOrganizations', () {
    final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'TONTINE Test')];
    final loadedState = OrganizationsLoaded(
      organizations: orgs,
      filteredOrganizations: orgs,
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'clears search when query is empty (with existing search)',
      build: buildBloc,
      seed: () => OrganizationsLoaded(
        organizations: orgs,
        filteredOrganizations: orgs,
        currentSearch: 'previous query',
      ),
      act: (b) => b.add(const SearchOrganizations('')),
      expect: () => [
        isA<OrganizationsLoaded>()
            .having((s) => s.currentSearch, 'currentSearch', isNull),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'performs local search then server search for non-empty query',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () {
        when(mockOrganizationService.searchLocal(any, any))
            .thenReturn([orgs.first]);
        when(mockGetOrganizations(
          page: anyNamed('page'),
          size: anyNamed('size'),
          recherche: anyNamed('recherche'),
        )).thenAnswer((_) async => [orgs.first]);
      },
      act: (b) => b.add(const SearchOrganizations('Association')),
      expect: () => [
        isA<OrganizationsLoaded>()
            .having((s) => s.currentSearch, 'search', 'Association'),
        isA<OrganizationsLoaded>()
            .having((s) => s.hasReachedMax, 'hasReachedMax', true),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'dispatches LoadOrganizations when not in loaded state',
      build: buildBloc,
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenAnswer((_) async => orgs),
      act: (b) => b.add(const SearchOrganizations('test')),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsLoaded>(),
      ],
    );
  });

  // ─── AdvancedSearchOrganizations ──────────────────────────────────────────

  group('AdvancedSearchOrganizations', () {
    final orgs = [_makeOrg()];

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Loading, Loaded] on success',
      build: buildBloc,
      setUp: () => when(mockRepository.searchOrganizations(
        nom: anyNamed('nom'),
        type: anyNamed('type'),
        statut: anyNamed('statut'),
        ville: anyNamed('ville'),
        region: anyNamed('region'),
        pays: anyNamed('pays'),
        page: anyNamed('page'),
        size: anyNamed('size'),
      )).thenAnswer((_) async => orgs),
      act: (b) => b.add(const AdvancedSearchOrganizations(nom: 'Test')),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsLoaded>(),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Loading, Error] on failure',
      build: buildBloc,
      setUp: () => when(mockRepository.searchOrganizations(
        nom: anyNamed('nom'),
        type: anyNamed('type'),
        statut: anyNamed('statut'),
        ville: anyNamed('ville'),
        region: anyNamed('region'),
        pays: anyNamed('pays'),
        page: anyNamed('page'),
        size: anyNamed('size'),
      )).thenThrow(Exception('server error')),
      act: (b) => b.add(const AdvancedSearchOrganizations(nom: 'Test')),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsError>(),
      ],
    );
  });

  // ─── LoadOrganizationById ─────────────────────────────────────────────────

  group('LoadOrganizationById', () {
    final org = _makeOrg();

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [OrganizationLoading, OrganizationLoaded] on success',
      build: buildBloc,
      setUp: () =>
          when(mockGetOrganizationById('org-1')).thenAnswer((_) async => org),
      act: (b) => b.add(const LoadOrganizationById('org-1')),
      expect: () => [
        const OrganizationLoading('org-1'),
        OrganizationLoaded(org),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [OrganizationLoading, OrganizationError] when not found (null)',
      build: buildBloc,
      setUp: () =>
          when(mockGetOrganizationById('missing')).thenAnswer((_) async => null),
      act: (b) => b.add(const LoadOrganizationById('missing')),
      expect: () => [
        const OrganizationLoading('missing'),
        isA<OrganizationError>().having((e) => e.organizationId, 'id', 'missing'),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [OrganizationLoading, OrganizationError] on exception',
      build: buildBloc,
      setUp: () =>
          when(mockGetOrganizationById('org-1')).thenThrow(Exception('fail')),
      act: (b) => b.add(const LoadOrganizationById('org-1')),
      expect: () => [
        const OrganizationLoading('org-1'),
        isA<OrganizationError>(),
      ],
    );
  });

  // ─── CreateOrganization ───────────────────────────────────────────────────

  group('CreateOrganization', () {
    final newOrg = _makeOrg(id: 'new-1', nom: 'New Org');

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Creating, Created] then triggers refresh on success',
      build: buildBloc,
      setUp: () {
        when(mockCreateOrganization(any)).thenAnswer((_) async => newOrg);
        when(mockGetOrganizations(
          page: anyNamed('page'),
          size: anyNamed('size'),
          recherche: anyNamed('recherche'),
        )).thenAnswer((_) async => [newOrg]);
      },
      act: (b) => b.add(CreateOrganization(newOrg)),
      expect: () => [
        const OrganizationCreating(),
        OrganizationCreated(newOrg),
        const OrganizationsLoading(),
        isA<OrganizationsLoaded>(),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Creating, Error] on failure',
      build: buildBloc,
      setUp: () =>
          when(mockCreateOrganization(any)).thenThrow(Exception('conflict')),
      act: (b) => b.add(CreateOrganization(newOrg)),
      expect: () => [
        const OrganizationCreating(),
        isA<OrganizationsError>()
            .having((e) => e.message, 'message', contains('création')),
      ],
    );
  });

  // ─── UpdateOrganization ───────────────────────────────────────────────────

  group('UpdateOrganization', () {
    final org = _makeOrg();
    final updated = _makeOrg(nom: 'Updated');
    final loadedState = OrganizationsLoaded(
      organizations: [org],
      filteredOrganizations: [org],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Updating, Updated] on success (previousState captured after first emit)',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockUpdateOrganization('org-1', any)).thenAnswer((_) async => updated),
      act: (b) => b.add(UpdateOrganization('org-1', updated)),
      expect: () => [
        const OrganizationUpdating('org-1'),
        OrganizationUpdated(updated),
        // Note: the BLoC captures previousState AFTER the first emit, so
        // previousState is OrganizationUpdating, not OrganizationsLoaded.
        // The list-update branch is only reached when starting from a loaded state
        // without any preceding emit — the inline emit order prevents it here.
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Updating, Error] on failure',
      build: buildBloc,
      setUp: () =>
          when(mockUpdateOrganization(any, any)).thenThrow(Exception('fail')),
      act: (b) => b.add(UpdateOrganization('org-1', org)),
      expect: () => [
        const OrganizationUpdating('org-1'),
        isA<OrganizationsError>(),
      ],
    );
  });

  // ─── DeleteOrganization ───────────────────────────────────────────────────

  group('DeleteOrganization', () {
    final org = _makeOrg();
    final loadedState = OrganizationsLoaded(
      organizations: [org],
      filteredOrganizations: [org],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Deleting, Deleted] on success (previousState captured after first emit)',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockDeleteOrganization('org-1')).thenAnswer((_) async {}),
      act: (b) => b.add(const DeleteOrganization('org-1')),
      expect: () => [
        const OrganizationDeleting('org-1'),
        const OrganizationDeleted('org-1'),
        // Note: the BLoC captures previousState AFTER the first emit,
        // so the list-removal branch (previousState is OrganizationsLoaded)
        // is not triggered here — consistent with UpdateOrganization behavior.
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Deleting, Error] on failure',
      build: buildBloc,
      setUp: () =>
          when(mockDeleteOrganization(any)).thenThrow(Exception('not found')),
      act: (b) => b.add(const DeleteOrganization('org-1')),
      expect: () => [
        const OrganizationDeleting('org-1'),
        isA<OrganizationsError>(),
      ],
    );
  });

  // ─── ActivateOrganization ─────────────────────────────────────────────────

  group('ActivateOrganization', () {
    final suspended = _makeOrg(statut: StatutOrganization.suspendue);
    final activated = _makeOrg(statut: StatutOrganization.active);
    final loadedState = OrganizationsLoaded(
      organizations: [suspended],
      filteredOrganizations: [suspended],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Activating, Activated, Loaded(updated)] on success',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockRepository.activateOrganization('org-1'))
          .thenAnswer((_) async => activated),
      act: (b) => b.add(const ActivateOrganization('org-1')),
      expect: () => [
        const OrganizationActivating('org-1'),
        OrganizationActivated(activated),
        isA<OrganizationsLoaded>()
            .having((s) => s.organizations.first.statut, 'statut', StatutOrganization.active),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Activating, Error] on failure',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.activateOrganization(any)).thenThrow(Exception('fail')),
      act: (b) => b.add(const ActivateOrganization('org-1')),
      expect: () => [
        const OrganizationActivating('org-1'),
        isA<OrganizationsError>(),
      ],
    );
  });

  // ─── SuspendOrganization ──────────────────────────────────────────────────

  group('SuspendOrganization', () {
    final active = _makeOrg();
    final suspended = _makeOrg(statut: StatutOrganization.suspendue);
    final loadedState = OrganizationsLoaded(
      organizations: [active],
      filteredOrganizations: [active],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Suspending, Suspended, Loaded(updated)] on success',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockRepository.suspendOrganization('org-1'))
          .thenAnswer((_) async => suspended),
      act: (b) => b.add(const SuspendOrganization('org-1')),
      expect: () => [
        const OrganizationSuspending('org-1'),
        OrganizationSuspended(suspended),
        isA<OrganizationsLoaded>()
            .having((s) => s.organizations.first.statut, 'statut', StatutOrganization.suspendue),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [Suspending, Error] on failure',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.suspendOrganization(any)).thenThrow(Exception('fail')),
      act: (b) => b.add(const SuspendOrganization('org-1')),
      expect: () => [
        const OrganizationSuspending('org-1'),
        isA<OrganizationsError>(),
      ],
    );
  });

  // ─── Filter & Sort ────────────────────────────────────────────────────────

  group('FilterOrganizationsByStatus', () {
    final orgs = [
      _makeOrg(statut: StatutOrganization.active),
      _makeOrg(id: 'org-2', statut: StatutOrganization.suspendue),
    ];
    final loadedState = OrganizationsLoaded(
      organizations: orgs,
      filteredOrganizations: orgs,
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits updated filtered list',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockOrganizationService.filterByStatus(any, StatutOrganization.active))
          .thenReturn([orgs.first]),
      act: (b) => b.add(const FilterOrganizationsByStatus(StatutOrganization.active)),
      expect: () => [
        isA<OrganizationsLoaded>()
            .having((s) => s.statusFilter, 'filter', StatutOrganization.active),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'does nothing when state is not loaded',
      build: buildBloc,
      act: (b) => b.add(const FilterOrganizationsByStatus(StatutOrganization.active)),
      expect: () => [],
    );
  });

  group('FilterOrganizationsByType', () {
    final orgs = [_makeOrg(), _makeOrg(id: 'org-2', type: 'TONTINE')];
    final loadedState = OrganizationsLoaded(
      organizations: orgs,
      filteredOrganizations: orgs,
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits updated filtered list with typeFilter',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockOrganizationService.filterByType(any, 'TONTINE')).thenReturn([orgs[1]]),
      act: (b) => b.add(const FilterOrganizationsByType('TONTINE')),
      expect: () => [
        isA<OrganizationsLoaded>()
            .having((s) => s.typeFilter, 'typeFilter', 'TONTINE'),
      ],
    );
  });

  group('SortOrganizations', () {
    final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'AAA')];
    final loadedState = OrganizationsLoaded(
      organizations: orgs,
      filteredOrganizations: orgs,
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'sorts by name ascending',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockOrganizationService.sortByName(any,
              ascending: anyNamed('ascending')))
          .thenReturn([orgs[1], orgs[0]]),
      act: (b) =>
          b.add(const SortOrganizations(OrganizationSortType.name)),
      expect: () => [
        isA<OrganizationsLoaded>()
            .having((s) => s.sortType, 'sortType', OrganizationSortType.name),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'sorts by creation date',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockOrganizationService.sortByCreationDate(any,
              ascending: anyNamed('ascending')))
          .thenReturn(orgs),
      act: (b) =>
          b.add(const SortOrganizations(OrganizationSortType.creationDate)),
      expect: () => [
        isA<OrganizationsLoaded>()
            .having((s) => s.sortType, 'sortType', OrganizationSortType.creationDate),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'sorts by member count',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () => when(mockOrganizationService.sortByMemberCount(any,
              ascending: anyNamed('ascending')))
          .thenReturn(orgs),
      act: (b) =>
          b.add(const SortOrganizations(OrganizationSortType.memberCount)),
      expect: () => [
        isA<OrganizationsLoaded>()
            .having((s) => s.sortType, 'sortType', OrganizationSortType.memberCount),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'does nothing when state is not loaded',
      build: buildBloc,
      act: (b) =>
          b.add(const SortOrganizations(OrganizationSortType.name)),
      expect: () => [],
    );
  });

  // ─── ClearOrganizationsFilters ────────────────────────────────────────────

  group('ClearOrganizationsFilters', () {
    final orgs = [_makeOrg()];
    final loadedState = OrganizationsLoaded(
      organizations: orgs,
      filteredOrganizations: orgs,
      statusFilter: StatutOrganization.active,
      typeFilter: 'TONTINE',
      currentSearch: 'some search',
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'clears all filters and restores full list',
      build: buildBloc,
      seed: () => loadedState,
      act: (b) => b.add(const ClearOrganizationsFilters()),
      expect: () => [
        isA<OrganizationsLoaded>()
            .having((s) => s.statusFilter, 'statusFilter', isNull)
            .having((s) => s.typeFilter, 'typeFilter', isNull)
            .having((s) => s.currentSearch, 'search', isNull),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'does nothing when state is not loaded',
      build: buildBloc,
      act: (b) => b.add(const ClearOrganizationsFilters()),
      expect: () => [],
    );
  });

  // ─── LoadOrganizationsStats ───────────────────────────────────────────────

  group('LoadOrganizationsStats', () {
    final stats = {'total': 5, 'actives': 3};

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [StatsLoading, StatsLoaded] on success',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.getOrganizationsStats()).thenAnswer((_) async => stats),
      act: (b) => b.add(const LoadOrganizationsStats()),
      expect: () => [
        const OrganizationsStatsLoading(),
        OrganizationsStatsLoaded(stats),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits [StatsLoading, StatsError] on failure',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.getOrganizationsStats()).thenThrow(Exception('fail')),
      act: (b) => b.add(const LoadOrganizationsStats()),
      expect: () => [
        const OrganizationsStatsLoading(),
        isA<OrganizationsStatsError>(),
      ],
    );
  });

  // ─── RefreshOrganizations ─────────────────────────────────────────────────

  group('RefreshOrganizations', () {
    final orgs = [_makeOrg()];

    blocTest<OrganizationsBloc, OrganizationsState>(
      'triggers reload via LoadOrganizations with useMesOnly when applicable',
      build: buildBloc,
      seed: () => OrganizationsLoaded(
        organizations: orgs,
        filteredOrganizations: orgs,
        useMesOnly: true,
      ),
      setUp: () =>
          when(mockRepository.getMesOrganisations()).thenAnswer((_) async => orgs),
      act: (b) => b.add(const RefreshOrganizations()),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsLoaded>().having((s) => s.useMesOnly, 'useMesOnly', true),
      ],
    );

    blocTest<OrganizationsBloc, OrganizationsState>(
      'triggers standard reload from initial state',
      build: buildBloc,
      setUp: () => when(mockGetOrganizations(
        page: anyNamed('page'),
        size: anyNamed('size'),
        recherche: anyNamed('recherche'),
      )).thenAnswer((_) async => orgs),
      act: (b) => b.add(const RefreshOrganizations()),
      expect: () => [
        const OrganizationsLoading(),
        isA<OrganizationsLoaded>(),
      ],
    );
  });

  // ─── ResetOrganizationsState ──────────────────────────────────────────────

  group('ResetOrganizationsState', () {
    blocTest<OrganizationsBloc, OrganizationsState>(
      'emits OrganizationsInitial',
      build: buildBloc,
      seed: () => OrganizationsLoaded(
        organizations: [_makeOrg()],
        filteredOrganizations: [_makeOrg()],
      ),
      act: (b) => b.add(const ResetOrganizationsState()),
      expect: () => [const OrganizationsInitial()],
    );
  });

  // ─── DioException cancel is swallowed ─────────────────────────────────────

  group('DioException.cancel handling', () {
    blocTest<OrganizationsBloc, OrganizationsState>(
      'LoadOrganizations: cancel exception produces no error state',
      build: buildBloc,
      setUp: () {
        when(mockGetOrganizations(
          page: anyNamed('page'),
          size: anyNamed('size'),
          recherche: anyNamed('recherche'),
        )).thenThrow(DioException(
          requestOptions: RequestOptions(path: '/test'),
          type: DioExceptionType.cancel,
        ));
      },
      act: (b) => b.add(const LoadOrganizations()),
      expect: () => [const OrganizationsLoading()],
    );
  });

  // ─── OrganizationsLoaded state helpers ────────────────────────────────────

  group('OrganizationsLoaded state helpers', () {
    final activeOrg = _makeOrg(statut: StatutOrganization.active, nombreMembres: 5);
    final suspendedOrg =
        _makeOrg(id: 'org-2', statut: StatutOrganization.suspendue, nombreMembres: 3);
    final state = OrganizationsLoaded(
      organizations: [activeOrg, suspendedOrg],
      filteredOrganizations: [activeOrg],
    );

    test('totalCount returns full organizations count', () {
      expect(state.totalCount, 2);
    });

    test('filteredCount returns filtered count', () {
      expect(state.filteredCount, 1);
    });

    test('quickStats calculates correctly', () {
      final stats = state.quickStats;
      expect(stats['total'], 2);
      expect(stats['actives'], 1);
      expect(stats['inactives'], 1);
      expect(stats['totalMembres'], 8);
    });

    test('hasFilters is false when no filters applied', () {
      final clean = OrganizationsLoaded(
        organizations: [activeOrg],
        filteredOrganizations: [activeOrg],
      );
      expect(clean.hasFilters, false);
    });

    test('hasFilters is true when statusFilter set', () {
      final filtered = OrganizationsLoaded(
        organizations: [activeOrg],
        filteredOrganizations: [activeOrg],
        statusFilter: StatutOrganization.active,
      );
      expect(filtered.hasFilters, true);
    });

    test('copyWith with clearSearch resets currentSearch', () {
      final withSearch = OrganizationsLoaded(
        organizations: [activeOrg],
        filteredOrganizations: [activeOrg],
        currentSearch: 'test',
      );
      final cleared = withSearch.copyWith(clearSearch: true);
      expect(cleared.currentSearch, isNull);
    });
  });
}
