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/members/bloc/membres_bloc.dart';
import 'package:unionflow_mobile_apps/features/members/bloc/membres_event.dart';
import 'package:unionflow_mobile_apps/features/members/bloc/membres_state.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_members.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_member_by_id.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/create_member.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/update_member.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/delete_member.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/search_members.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_member_stats.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
import 'package:unionflow_mobile_apps/shared/models/membre_search_result.dart';
import 'package:unionflow_mobile_apps/shared/models/membre_search_criteria.dart';

@GenerateMocks([
  GetMembers,
  GetMemberById,
  CreateMember,
  UpdateMember,
  DeleteMember,
  SearchMembers,
  GetMemberStats,
  IMembreRepository,
])
import 'membres_bloc_test.mocks.dart';

void main() {
  late MembresBloc bloc;
  late MockGetMembers mockGetMembers;
  late MockGetMemberById mockGetMemberById;
  late MockCreateMember mockCreateMember;
  late MockUpdateMember mockUpdateMember;
  late MockDeleteMember mockDeleteMember;
  late MockSearchMembers mockSearchMembers;
  late MockGetMemberStats mockGetMemberStats;
  late MockIMembreRepository mockRepository;

  const orgId = 'org-123';

  MembreSearchResult emptyResult() => MembreSearchResult(
        membres: [],
        totalElements: 0,
        totalPages: 0,
        currentPage: 0,
        pageSize: 20,
        numberOfElements: 0,
        hasNext: false,
        hasPrevious: false,
        isFirst: true,
        isLast: true,
        criteria: MembreSearchCriteria(),
        executionTimeMs: 0,
      );

  setUp(() {
    mockGetMembers = MockGetMembers();
    mockGetMemberById = MockGetMemberById();
    mockCreateMember = MockCreateMember();
    mockUpdateMember = MockUpdateMember();
    mockDeleteMember = MockDeleteMember();
    mockSearchMembers = MockSearchMembers();
    mockGetMemberStats = MockGetMemberStats();
    mockRepository = MockIMembreRepository();

    bloc = MembresBloc(
      mockGetMembers,
      mockGetMemberById,
      mockCreateMember,
      mockUpdateMember,
      mockDeleteMember,
      mockSearchMembers,
      mockGetMemberStats,
      mockRepository,
    );
  });

  tearDown(() => bloc.close());

  // ─────────────────────────────────────────────────────────────────────────
  // SuperAdmin — accès global sans filtre organisation
  // ─────────────────────────────────────────────────────────────────────────

  group('LoadMembres — SuperAdmin (sans organisationId)', () {
    blocTest<MembresBloc, MembresState>(
      'appelle GetMembers sans filtre org et émet MembresLoaded sans organisationId',
      build: () {
        when(mockGetMembers(
          page: anyNamed('page'),
          size: anyNamed('size'),
          recherche: anyNamed('recherche'),
        )).thenAnswer((_) async => emptyResult());
        return bloc;
      },
      act: (b) => b.add(const LoadMembres()),
      expect: () => [
        const MembresLoading(),
        isA<MembresLoaded>().having(
          (s) => s.organisationId,
          'organisationId',
          isNull,
        ),
      ],
      verify: (_) {
        // GetMembers doit être appelé avec les bons paramètres
        final captured = verify(mockGetMembers(
          page: captureAnyNamed('page'),
          size: captureAnyNamed('size'),
          recherche: captureAnyNamed('recherche'),
        )).captured;
        expect(captured[0], equals(0));    // page
        expect(captured[1], equals(20));   // size
        expect(captured[2], isNull);       // recherche

        // SearchMembers ne doit jamais être appelé
        verifyNever(mockSearchMembers(
          criteria: anyNamed('criteria'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        ));
      },
    );

    blocTest<MembresBloc, MembresState>(
      'transmet le terme de recherche à GetMembers',
      build: () {
        when(mockGetMembers(
          page: anyNamed('page'),
          size: anyNamed('size'),
          recherche: anyNamed('recherche'),
        )).thenAnswer((_) async => emptyResult());
        return bloc;
      },
      act: (b) => b.add(const LoadMembres(recherche: 'Jean')),
      expect: () => [
        const MembresLoading(),
        isA<MembresLoaded>(),
      ],
      verify: (_) {
        final captured = verify(mockGetMembers(
          page: captureAnyNamed('page'),
          size: captureAnyNamed('size'),
          recherche: captureAnyNamed('recherche'),
        )).captured;
        expect(captured[2], equals('Jean')); // recherche

        verifyNever(mockSearchMembers(
          criteria: anyNamed('criteria'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        ));
      },
    );
  });

  // ─────────────────────────────────────────────────────────────────────────
  // OrgAdmin — accès limité à son organisation
  // ─────────────────────────────────────────────────────────────────────────

  group('LoadMembres — OrgAdmin (avec organisationId)', () {
    blocTest<MembresBloc, MembresState>(
      'appelle SearchMembers avec organisationIds et émet MembresLoaded avec organisationId',
      build: () {
        when(mockSearchMembers(
          criteria: anyNamed('criteria'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        )).thenAnswer((_) async => emptyResult());
        return bloc;
      },
      act: (b) => b.add(const LoadMembres(organisationId: orgId)),
      expect: () => [
        const MembresLoading(),
        isA<MembresLoaded>().having(
          (s) => s.organisationId,
          'organisationId',
          equals(orgId),
        ),
      ],
      verify: (_) {
        // GetMembers ne doit JAMAIS être appelé pour un OrgAdmin
        verifyNever(mockGetMembers(
          page: anyNamed('page'),
          size: anyNamed('size'),
          recherche: anyNamed('recherche'),
        ));

        // SearchMembers doit être appelé avec l'organisationId dans les critères
        final captured = verify(mockSearchMembers(
          criteria: captureAnyNamed('criteria'),
          page: captureAnyNamed('page'),
          size: captureAnyNamed('size'),
        )).captured;

        final criteria = captured[0] as MembreSearchCriteria;
        expect(criteria.organisationIds, equals([orgId]));
        expect(criteria.query, isNull);
        expect(captured[1], equals(0));   // page
        expect(captured[2], equals(20));  // size
      },
    );

    blocTest<MembresBloc, MembresState>(
      'OrgAdmin avec recherche : ajoute query aux critères',
      build: () {
        when(mockSearchMembers(
          criteria: anyNamed('criteria'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        )).thenAnswer((_) async => emptyResult());
        return bloc;
      },
      act: (b) => b.add(const LoadMembres(organisationId: orgId, recherche: 'Dupont')),
      expect: () => [
        const MembresLoading(),
        isA<MembresLoaded>(),
      ],
      verify: (_) {
        final captured = verify(mockSearchMembers(
          criteria: captureAnyNamed('criteria'),
          page: captureAnyNamed('page'),
          size: captureAnyNamed('size'),
        )).captured;

        final criteria = captured[0] as MembreSearchCriteria;
        expect(criteria.organisationIds, equals([orgId]));
        expect(criteria.query, equals('Dupont'));
      },
    );

    blocTest<MembresBloc, MembresState>(
      'OrgAdmin avec recherche vide : query non transmis',
      build: () {
        when(mockSearchMembers(
          criteria: anyNamed('criteria'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        )).thenAnswer((_) async => emptyResult());
        return bloc;
      },
      act: (b) => b.add(const LoadMembres(organisationId: orgId, recherche: '')),
      expect: () => [const MembresLoading(), isA<MembresLoaded>()],
      verify: (_) {
        final captured = verify(mockSearchMembers(
          criteria: captureAnyNamed('criteria'),
          page: captureAnyNamed('page'),
          size: captureAnyNamed('size'),
        )).captured;

        final criteria = captured[0] as MembreSearchCriteria;
        expect(criteria.organisationIds, equals([orgId]));
        // recherche vide → query null (pas transmis aux critères)
        expect(criteria.query, isNull);
      },
    );

    blocTest<MembresBloc, MembresState>(
      'OrgAdmin : la pagination conserve l organisationId',
      build: () {
        when(mockSearchMembers(
          criteria: anyNamed('criteria'),
          page: anyNamed('page'),
          size: anyNamed('size'),
        )).thenAnswer((_) async => emptyResult());
        return bloc;
      },
      act: (b) => b.add(const LoadMembres(organisationId: orgId, page: 1)),
      expect: () => [
        const MembresLoading(),
        isA<MembresLoaded>().having(
          (s) => s.organisationId,
          'organisationId',
          equals(orgId),
        ),
      ],
      verify: (_) {
        final captured = verify(mockSearchMembers(
          criteria: captureAnyNamed('criteria'),
          page: captureAnyNamed('page'),
          size: captureAnyNamed('size'),
        )).captured;
        expect(captured[1], equals(1)); // page = 1
      },
    );
  });

  // ─────────────────────────────────────────────────────────────────────────
  // MembresLoaded.copyWith
  // ─────────────────────────────────────────────────────────────────────────

  // ─────────────────────────────────────────────────────────────────────────
  // ResetMotDePasse
  // ─────────────────────────────────────────────────────────────────────────

  group('ResetMotDePasse', () {
    MembreCompletModel membreAvecPassword() => MembreCompletModel(
          id: 'membre-id-1',
          prenom: 'Jean',
          nom: 'Dupont',
          email: 'jean.dupont@test.com',
          motDePasseTemporaire: 'NewP@ss1234',
        );

    blocTest<MembresBloc, MembresState>(
      'émet [MembresLoading, MotDePasseReinitialise] en cas de succès',
      build: () {
        when(mockRepository.resetMotDePasse('membre-id-1'))
            .thenAnswer((_) async => membreAvecPassword());
        return bloc;
      },
      act: (b) => b.add(const ResetMotDePasse('membre-id-1')),
      expect: () => [
        const MembresLoading(),
        isA<MotDePasseReinitialise>()
            .having((s) => s.membre.id, 'id', 'membre-id-1')
            .having((s) => s.membre.motDePasseTemporaire, 'password', 'NewP@ss1234'),
      ],
      verify: (_) {
        verify(mockRepository.resetMotDePasse('membre-id-1')).called(1);
      },
    );

    blocTest<MembresBloc, MembresState>(
      'émet [MembresLoading, MembresError] si le repository lève une exception',
      build: () {
        when(mockRepository.resetMotDePasse('membre-id-1'))
            .thenThrow(Exception('Membre sans compte Keycloak'));
        return bloc;
      },
      act: (b) => b.add(const ResetMotDePasse('membre-id-1')),
      expect: () => [
        const MembresLoading(),
        isA<MembresError>(),
      ],
    );
  });

  group('MembresLoaded.copyWith', () {
    test('preserve organisationId si non fourni', () {
      const state = MembresLoaded(
        membres: [],
        totalElements: 5,
        totalPages: 1,
        organisationId: orgId,
      );
      final copy = state.copyWith(totalElements: 10);
      expect(copy.organisationId, equals(orgId));
      expect(copy.totalElements, equals(10));
    });

    test('met à jour organisationId si fourni', () {
      const state = MembresLoaded(
        membres: [],
        totalElements: 5,
        totalPages: 1,
        organisationId: orgId,
      );
      final copy = state.copyWith(organisationId: 'new-org');
      expect(copy.organisationId, equals('new-org'));
    });

    test('organisationId null par défaut si non défini', () {
      const state = MembresLoaded(
        membres: [],
        totalElements: 0,
        totalPages: 0,
      );
      expect(state.organisationId, isNull);
      final copy = state.copyWith(totalElements: 1, totalPages: 1);
      expect(copy.organisationId, isNull);
    });
  });
}
