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/org_types_bloc.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/entities/type_reference_entity.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/create_org_type.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/delete_org_type.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_org_types.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/update_org_type.dart';

@GenerateMocks([GetOrgTypes, CreateOrgType, UpdateOrgType, DeleteOrgType])
import 'org_types_bloc_test.mocks.dart';

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

TypeReferenceEntity _makeType({
  String id = 'type-1',
  String code = 'ASSOCIATION',
  String libelle = 'Association',
  int ordre = 0,
}) =>
    TypeReferenceEntity(
      id: id,
      domaine: 'TYPE_ORGANISATION',
      code: code,
      libelle: libelle,
      ordreAffichage: ordre,
    );

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

void main() {
  late MockGetOrgTypes mockGetOrgTypes;
  late MockCreateOrgType mockCreateOrgType;
  late MockUpdateOrgType mockUpdateOrgType;
  late MockDeleteOrgType mockDeleteOrgType;

  OrgTypesBloc buildBloc() => OrgTypesBloc(
        mockGetOrgTypes,
        mockCreateOrgType,
        mockUpdateOrgType,
        mockDeleteOrgType,
      );

  setUp(() {
    mockGetOrgTypes = MockGetOrgTypes();
    mockCreateOrgType = MockCreateOrgType();
    mockUpdateOrgType = MockUpdateOrgType();
    mockDeleteOrgType = MockDeleteOrgType();
  });

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

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

  // ─── LoadOrgTypes ─────────────────────────────────────────────────────────

  group('LoadOrgTypes', () {
    final types = [_makeType(), _makeType(id: 'type-2', code: 'TONTINE', libelle: 'Tontine')];

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Loading, Loaded] on success',
      build: buildBloc,
      setUp: () => when(mockGetOrgTypes()).thenAnswer((_) async => types),
      act: (b) => b.add(const LoadOrgTypes()),
      expect: () => [
        const OrgTypesLoading(),
        OrgTypesLoaded(types),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Loading, Loaded(empty)] when no types exist',
      build: buildBloc,
      setUp: () => when(mockGetOrgTypes()).thenAnswer((_) async => []),
      act: (b) => b.add(const LoadOrgTypes()),
      expect: () => [
        const OrgTypesLoading(),
        const OrgTypesLoaded([]),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Loading, Error] on exception',
      build: buildBloc,
      setUp: () =>
          when(mockGetOrgTypes()).thenThrow(Exception('network error')),
      act: (b) => b.add(const LoadOrgTypes()),
      expect: () => [
        const OrgTypesLoading(),
        isA<OrgTypesError>().having((e) => e.message, 'message', isNotEmpty),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'swallows DioException.cancel',
      build: buildBloc,
      setUp: () => when(mockGetOrgTypes()).thenThrow(DioException(
        requestOptions: RequestOptions(path: '/types'),
        type: DioExceptionType.cancel,
      )),
      act: (b) => b.add(const LoadOrgTypes()),
      expect: () => [const OrgTypesLoading()],
    );
  });

  // ─── CreateOrgTypeEvent ───────────────────────────────────────────────────

  group('CreateOrgTypeEvent', () {
    final existing = [_makeType()];
    final newType = _makeType(id: 'type-new', code: 'MUTUELLE', libelle: 'Mutuelle');
    final updatedList = [...existing, newType];

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Operating, Success] with refreshed list on success',
      build: buildBloc,
      seed: () => OrgTypesLoaded(existing),
      setUp: () {
        when(mockCreateOrgType(
          code: anyNamed('code'),
          libelle: anyNamed('libelle'),
          description: anyNamed('description'),
          couleur: anyNamed('couleur'),
          ordreAffichage: anyNamed('ordreAffichage'),
        )).thenAnswer((_) async => newType);
        when(mockGetOrgTypes()).thenAnswer((_) async => updatedList);
      },
      act: (b) => b.add(const CreateOrgTypeEvent(
        code: 'MUTUELLE',
        libelle: 'Mutuelle',
      )),
      expect: () => [
        isA<OrgTypeOperating>()
            .having((s) => s.types, 'types', existing),
        isA<OrgTypeSuccess>()
            .having((s) => s.message, 'message', contains('créé'))
            .having((s) => s.types.length, 'count', 2),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Operating, OperationError] on create failure',
      build: buildBloc,
      seed: () => OrgTypesLoaded(existing),
      setUp: () {
        when(mockCreateOrgType(
          code: anyNamed('code'),
          libelle: anyNamed('libelle'),
          description: anyNamed('description'),
          couleur: anyNamed('couleur'),
          ordreAffichage: anyNamed('ordreAffichage'),
        )).thenThrow(Exception('duplicate code'));
      },
      act: (b) => b.add(const CreateOrgTypeEvent(
        code: 'MUTUELLE',
        libelle: 'Mutuelle',
      )),
      expect: () => [
        isA<OrgTypeOperating>(),
        isA<OrgTypeOperationError>()
            .having((s) => s.message, 'message', isNotEmpty)
            .having((s) => s.types, 'types preserved', existing),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'passes optional fields (description, couleur, ordreAffichage) to use case',
      build: buildBloc,
      seed: () => const OrgTypesInitial(),
      setUp: () {
        when(mockCreateOrgType(
          code: anyNamed('code'),
          libelle: anyNamed('libelle'),
          description: anyNamed('description'),
          couleur: anyNamed('couleur'),
          ordreAffichage: anyNamed('ordreAffichage'),
        )).thenAnswer((_) async => newType);
        when(mockGetOrgTypes()).thenAnswer((_) async => [newType]);
      },
      act: (b) => b.add(const CreateOrgTypeEvent(
        code: 'MUTUELLE',
        libelle: 'Mutuelle',
        description: 'Une mutuelle',
        couleur: '#FF5733',
        ordreAffichage: 2,
      )),
      verify: (_) {
        verify(mockCreateOrgType(
          code: 'MUTUELLE',
          libelle: 'Mutuelle',
          description: 'Une mutuelle',
          couleur: '#FF5733',
          ordreAffichage: 2,
        )).called(1);
      },
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'swallows DioException.cancel during create',
      build: buildBloc,
      setUp: () {
        when(mockCreateOrgType(
          code: anyNamed('code'),
          libelle: anyNamed('libelle'),
          description: anyNamed('description'),
          couleur: anyNamed('couleur'),
          ordreAffichage: anyNamed('ordreAffichage'),
        )).thenThrow(DioException(
          requestOptions: RequestOptions(path: '/types'),
          type: DioExceptionType.cancel,
        ));
      },
      act: (b) => b.add(const CreateOrgTypeEvent(
        code: 'MUTUELLE',
        libelle: 'Mutuelle',
      )),
      expect: () => [isA<OrgTypeOperating>()],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'uses empty list as current when state is not OrgTypesLoaded',
      build: buildBloc,
      setUp: () {
        when(mockCreateOrgType(
          code: anyNamed('code'),
          libelle: anyNamed('libelle'),
          description: anyNamed('description'),
          couleur: anyNamed('couleur'),
          ordreAffichage: anyNamed('ordreAffichage'),
        )).thenAnswer((_) async => newType);
        when(mockGetOrgTypes()).thenAnswer((_) async => [newType]);
      },
      act: (b) => b.add(const CreateOrgTypeEvent(
        code: 'MUTUELLE',
        libelle: 'Mutuelle',
      )),
      expect: () => [
        isA<OrgTypeOperating>().having((s) => s.types, 'types', isEmpty),
        isA<OrgTypeSuccess>(),
      ],
    );
  });

  // ─── UpdateOrgTypeEvent ───────────────────────────────────────────────────

  group('UpdateOrgTypeEvent', () {
    final existing = [_makeType()];
    final updatedType = _makeType(libelle: 'Association Modifiée');

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Operating, Success] with refreshed list on success',
      build: buildBloc,
      seed: () => OrgTypesLoaded(existing),
      setUp: () {
        when(mockUpdateOrgType(
          id: anyNamed('id'),
          code: anyNamed('code'),
          libelle: anyNamed('libelle'),
          description: anyNamed('description'),
          couleur: anyNamed('couleur'),
          ordreAffichage: anyNamed('ordreAffichage'),
        )).thenAnswer((_) async => updatedType);
        when(mockGetOrgTypes()).thenAnswer((_) async => [updatedType]);
      },
      act: (b) => b.add(const UpdateOrgTypeEvent(
        id: 'type-1',
        code: 'ASSOCIATION',
        libelle: 'Association Modifiée',
      )),
      expect: () => [
        isA<OrgTypeOperating>().having((s) => s.types, 'types', existing),
        isA<OrgTypeSuccess>()
            .having((s) => s.message, 'message', contains('modifié'))
            .having((s) => s.types.first.libelle, 'libelle', 'Association Modifiée'),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Operating, OperationError] on update failure',
      build: buildBloc,
      seed: () => OrgTypesLoaded(existing),
      setUp: () {
        when(mockUpdateOrgType(
          id: anyNamed('id'),
          code: anyNamed('code'),
          libelle: anyNamed('libelle'),
          description: anyNamed('description'),
          couleur: anyNamed('couleur'),
          ordreAffichage: anyNamed('ordreAffichage'),
        )).thenThrow(Exception('not found'));
      },
      act: (b) => b.add(const UpdateOrgTypeEvent(
        id: 'type-1',
        code: 'ASSOCIATION',
        libelle: 'Association Modifiée',
      )),
      expect: () => [
        isA<OrgTypeOperating>(),
        isA<OrgTypeOperationError>().having((s) => s.types, 'types preserved', existing),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'swallows DioException.cancel during update',
      build: buildBloc,
      setUp: () {
        when(mockUpdateOrgType(
          id: anyNamed('id'),
          code: anyNamed('code'),
          libelle: anyNamed('libelle'),
          description: anyNamed('description'),
          couleur: anyNamed('couleur'),
          ordreAffichage: anyNamed('ordreAffichage'),
        )).thenThrow(DioException(
          requestOptions: RequestOptions(path: '/types'),
          type: DioExceptionType.cancel,
        ));
      },
      act: (b) => b.add(const UpdateOrgTypeEvent(
        id: 'type-1',
        code: 'ASSOCIATION',
        libelle: 'Association',
      )),
      expect: () => [isA<OrgTypeOperating>()],
    );
  });

  // ─── DeleteOrgTypeEvent ───────────────────────────────────────────────────

  group('DeleteOrgTypeEvent', () {
    final existing = [_makeType(), _makeType(id: 'type-2', code: 'TONTINE', libelle: 'Tontine')];

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Operating, Success] with refreshed list on success',
      build: buildBloc,
      seed: () => OrgTypesLoaded(existing),
      setUp: () {
        when(mockDeleteOrgType('type-1')).thenAnswer((_) async {});
        when(mockGetOrgTypes())
            .thenAnswer((_) async => [existing[1]]); // one removed
      },
      act: (b) => b.add(const DeleteOrgTypeEvent('type-1')),
      expect: () => [
        isA<OrgTypeOperating>().having((s) => s.types, 'types', existing),
        isA<OrgTypeSuccess>()
            .having((s) => s.message, 'message', contains('supprimé'))
            .having((s) => s.types.length, 'count', 1),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'emits [Operating, OperationError] on delete failure',
      build: buildBloc,
      seed: () => OrgTypesLoaded(existing),
      setUp: () =>
          when(mockDeleteOrgType(any)).thenThrow(Exception('still referenced')),
      act: (b) => b.add(const DeleteOrgTypeEvent('type-1')),
      expect: () => [
        isA<OrgTypeOperating>(),
        isA<OrgTypeOperationError>()
            .having((s) => s.types, 'types preserved', existing),
      ],
    );

    blocTest<OrgTypesBloc, OrgTypesState>(
      'swallows DioException.cancel during delete',
      build: buildBloc,
      setUp: () => when(mockDeleteOrgType(any)).thenThrow(DioException(
        requestOptions: RequestOptions(path: '/types'),
        type: DioExceptionType.cancel,
      )),
      act: (b) => b.add(const DeleteOrgTypeEvent('type-1')),
      expect: () => [isA<OrgTypeOperating>()],
    );
  });

  // ─── State equality ───────────────────────────────────────────────────────

  group('State equality', () {
    final types = [_makeType()];

    test('OrgTypesLoaded with same list are equal', () {
      expect(OrgTypesLoaded(types), OrgTypesLoaded(types));
    });

    test('OrgTypesError with same message are equal', () {
      expect(const OrgTypesError('fail'), const OrgTypesError('fail'));
    });

    test('OrgTypeSuccess with same data are equal', () {
      expect(
        OrgTypeSuccess(types: types, message: 'ok'),
        OrgTypeSuccess(types: types, message: 'ok'),
      );
    });

    test('OrgTypeOperationError with same data are equal', () {
      expect(
        OrgTypeOperationError(types: types, message: 'err'),
        OrgTypeOperationError(types: types, message: 'err'),
      );
    });
  });

  // ─── TypeReferenceEntity helpers ──────────────────────────────────────────

  group('TypeReferenceEntity', () {
    test('default values are correctly set', () {
      final entity = TypeReferenceEntity(
        id: 'id',
        domaine: 'TYPE_ORGANISATION',
        code: 'ASSO',
        libelle: 'Association',
      );
      expect(entity.ordreAffichage, 0);
      expect(entity.estDefaut, false);
      expect(entity.estSysteme, false);
      expect(entity.actif, true);
    });

    test('equality holds for identical entities', () {
      final a = _makeType();
      final b = _makeType();
      expect(a, b);
    });
  });
}
