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/profile/presentation/bloc/profile_bloc.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/usecases/get_profile.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/usecases/update_profile.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/repositories/profile_repository.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';

@GenerateMocks([GetProfile, UpdateProfile, IProfileRepository])
import 'profile_bloc_test.mocks.dart';

// ─── Fixtures ────────────────────────────────────────────────────────────────

MembreCompletModel _membre({String id = 'membre-1'}) => MembreCompletModel(
      id: id,
      nom: 'Dupont',
      prenom: 'Jean',
      email: 'jean.dupont@test.com',
    );

DioException _dioError(int statusCode) => DioException(
      requestOptions: RequestOptions(path: '/api/membres/me'),
      response: Response(
        requestOptions: RequestOptions(path: '/api/membres/me'),
        statusCode: statusCode,
      ),
      type: DioExceptionType.badResponse,
    );

DioException _networkError() => DioException(
      requestOptions: RequestOptions(path: '/api/membres/me'),
      type: DioExceptionType.connectionTimeout,
    );

// ─── Tests ───────────────────────────────────────────────────────────────────

void main() {
  late ProfileBloc bloc;
  late MockGetProfile mockGetProfile;
  late MockUpdateProfile mockUpdateProfile;
  late MockIProfileRepository mockRepository;

  setUp(() {
    mockGetProfile = MockGetProfile();
    mockUpdateProfile = MockUpdateProfile();
    mockRepository = MockIProfileRepository();

    bloc = ProfileBloc(mockGetProfile, mockUpdateProfile, mockRepository);
  });

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

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

  test('initial state is ProfileInitial', () {
    expect(bloc.state, isA<ProfileInitial>());
  });

  // ─── LoadMe ─────────────────────────────────────────────────────────────────

  group('LoadMe', () {
    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileLoaded] on success',
      build: () {
        when(mockGetProfile()).thenAnswer((_) async => _membre());
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileLoaded>()
            .having((s) => s.membre.id, 'id', 'membre-1')
            .having((s) => s.membre.email, 'email', 'jean.dupont@test.com'),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileNotFound] when getProfile returns null',
      build: () {
        when(mockGetProfile()).thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [isA<ProfileLoading>(), isA<ProfileNotFound>()],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileError] on DioException (401)',
      build: () {
        when(mockGetProfile()).thenThrow(_dioError(401));
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>().having(
          (s) => s.message,
          'message',
          contains('Non autorisé'),
        ),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileError] on DioException (403)',
      build: () {
        when(mockGetProfile()).thenThrow(_dioError(403));
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Accès refusé')),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileError] on DioException (404)',
      build: () {
        when(mockGetProfile()).thenThrow(_dioError(404));
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('non trouvé')),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileError] on DioException (500)',
      build: () {
        when(mockGetProfile()).thenThrow(_dioError(500));
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Erreur serveur')),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileError] on network timeout',
      build: () {
        when(mockGetProfile()).thenThrow(_networkError());
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>().having(
          (s) => s.message,
          'message',
          contains('Délai de connexion'),
        ),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileError] on generic exception',
      build: () {
        when(mockGetProfile()).thenThrow(Exception('Unexpected failure'));
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>().having(
          (s) => s.message,
          'message',
          contains('Erreur lors du chargement'),
        ),
      ],
    );
  });

  // ─── LoadMyProfile ──────────────────────────────────────────────────────────

  group('LoadMyProfile', () {
    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileLoaded] on success',
      build: () {
        when(mockRepository.getProfileByEmail('jean@test.com'))
            .thenAnswer((_) async => _membre());
        return bloc;
      },
      act: (b) => b.add(const LoadMyProfile('jean@test.com')),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileLoaded>().having((s) => s.membre.id, 'id', 'membre-1'),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileNotFound] when repository returns null',
      build: () {
        when(mockRepository.getProfileByEmail(any))
            .thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) => b.add(const LoadMyProfile('unknown@test.com')),
      expect: () => [isA<ProfileLoading>(), isA<ProfileNotFound>()],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileError] on DioException',
      build: () {
        when(mockRepository.getProfileByEmail(any))
            .thenThrow(_dioError(404));
        return bloc;
      },
      act: (b) => b.add(const LoadMyProfile('jean@test.com')),
      expect: () => [isA<ProfileLoading>(), isA<ProfileError>()],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileLoading, ProfileError] on generic exception',
      build: () {
        when(mockRepository.getProfileByEmail(any))
            .thenThrow(Exception('DB error'));
        return bloc;
      },
      act: (b) => b.add(const LoadMyProfile('jean@test.com')),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Erreur lors du chargement')),
      ],
    );
  });

  // ─── UpdateMyProfile ────────────────────────────────────────────────────────

  group('UpdateMyProfile', () {
    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileUpdating, ProfileUpdated] when state is ProfileLoaded on success',
      build: () {
        final updatedMembre = _membre(id: 'membre-1');
        when(mockUpdateProfile('membre-1', any))
            .thenAnswer((_) async => updatedMembre);
        return bloc;
      },
      seed: () => ProfileLoaded(_membre()),
      act: (b) => b.add(UpdateMyProfile(
        membreId: 'membre-1',
        membre: _membre(),
      )),
      expect: () => [
        isA<ProfileUpdating>().having((s) => s.membre.id, 'id', 'membre-1'),
        isA<ProfileUpdated>().having((s) => s.membre.id, 'id', 'membre-1'),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileUpdated] from ProfileInitial (no ProfileUpdating)',
      build: () {
        when(mockUpdateProfile('membre-1', any))
            .thenAnswer((_) async => _membre());
        return bloc;
      },
      // No seed → initial state is ProfileInitial (not ProfileLoaded)
      act: (b) => b.add(UpdateMyProfile(
        membreId: 'membre-1',
        membre: _membre(),
      )),
      expect: () => [isA<ProfileUpdated>()],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileUpdating, ProfileLoaded, ProfileError] on DioException when state is ProfileLoaded',
      build: () {
        when(mockUpdateProfile(any, any)).thenThrow(_dioError(400));
        return bloc;
      },
      seed: () => ProfileLoaded(_membre()),
      act: (b) => b.add(UpdateMyProfile(
        membreId: 'membre-1',
        membre: _membre(),
      )),
      expect: () => [
        isA<ProfileUpdating>(),
        isA<ProfileLoaded>(), // restored previous state
        isA<ProfileError>(),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileError] on DioException when state is not ProfileLoaded',
      build: () {
        when(mockUpdateProfile(any, any)).thenThrow(_dioError(500));
        return bloc;
      },
      act: (b) => b.add(UpdateMyProfile(
        membreId: 'membre-1',
        membre: _membre(),
      )),
      expect: () => [isA<ProfileError>()],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [ProfileUpdating, ProfileError] on generic exception when state is ProfileLoaded',
      build: () {
        when(mockUpdateProfile(any, any))
            .thenThrow(Exception('Serialization error'));
        return bloc;
      },
      seed: () => ProfileLoaded(_membre()),
      act: (b) => b.add(UpdateMyProfile(
        membreId: 'membre-1',
        membre: _membre(),
      )),
      expect: () => [
        isA<ProfileUpdating>(),
        isA<ProfileLoaded>(),
        isA<ProfileError>().having(
          (s) => s.message,
          'message',
          contains('mise à jour'),
        ),
      ],
    );
  });

  // ─── ChangePassword ─────────────────────────────────────────────────────────

  group('ChangePassword', () {
    blocTest<ProfileBloc, ProfileState>(
      'emits [PasswordChanging, PasswordChanged] on success from ProfileInitial',
      build: () {
        when(mockRepository.changePassword('membre-1', 'old123', 'new456'))
            .thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const ChangePassword(
        membreId: 'membre-1',
        oldPassword: 'old123',
        newPassword: 'new456',
      )),
      expect: () => [isA<PasswordChanging>(), isA<PasswordChanged>()],
    );

    blocTest<ProfileBloc, ProfileState>(
      'restores ProfileLoaded after PasswordChanged when previous state was ProfileLoaded',
      build: () {
        when(mockRepository.changePassword(any, any, any))
            .thenAnswer((_) async {});
        return bloc;
      },
      seed: () => ProfileLoaded(_membre()),
      act: (b) => b.add(const ChangePassword(
        membreId: 'membre-1',
        oldPassword: 'old123',
        newPassword: 'new456',
      )),
      expect: () => [
        isA<PasswordChanging>(),
        isA<PasswordChanged>(),
        isA<ProfileLoaded>(), // restored
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [PasswordChanging, ProfileError] on DioException (403)',
      build: () {
        when(mockRepository.changePassword(any, any, any))
            .thenThrow(_dioError(403));
        return bloc;
      },
      act: (b) => b.add(const ChangePassword(
        membreId: 'membre-1',
        oldPassword: 'wrong',
        newPassword: 'new',
      )),
      expect: () => [
        isA<PasswordChanging>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Accès refusé')),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [PasswordChanging, ProfileError, ProfileLoaded] on DioException when state is ProfileLoaded',
      build: () {
        when(mockRepository.changePassword(any, any, any))
            .thenThrow(_dioError(401));
        return bloc;
      },
      seed: () => ProfileLoaded(_membre()),
      act: (b) => b.add(const ChangePassword(
        membreId: 'membre-1',
        oldPassword: 'old',
        newPassword: 'new',
      )),
      expect: () => [
        isA<PasswordChanging>(),
        isA<ProfileError>(),
        isA<ProfileLoaded>(), // restored
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [PasswordChanging, ProfileError] on generic exception',
      build: () {
        when(mockRepository.changePassword(any, any, any))
            .thenThrow(Exception('Wrong old password'));
        return bloc;
      },
      act: (b) => b.add(const ChangePassword(
        membreId: 'membre-1',
        oldPassword: 'wrong',
        newPassword: 'new456',
      )),
      expect: () => [
        isA<PasswordChanging>(),
        isA<ProfileError>(),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'strips Exception: prefix from generic error message',
      build: () {
        when(mockRepository.changePassword(any, any, any))
            .thenThrow(Exception('Custom error message'));
        return bloc;
      },
      act: (b) => b.add(const ChangePassword(
        membreId: 'membre-1',
        oldPassword: 'x',
        newPassword: 'y',
      )),
      expect: () => [
        isA<PasswordChanging>(),
        isA<ProfileError>().having(
          (s) => s.message,
          'message',
          isNot(contains('Exception:')),
        ),
      ],
    );
  });

  // ─── DeleteAccount ──────────────────────────────────────────────────────────

  group('DeleteAccount', () {
    blocTest<ProfileBloc, ProfileState>(
      'emits [AccountDeleting, AccountDeleted] on success',
      build: () {
        when(mockRepository.deleteAccount('membre-1'))
            .thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const DeleteAccount('membre-1')),
      expect: () => [isA<AccountDeleting>(), isA<AccountDeleted>()],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [AccountDeleting, ProfileError] on DioException (401)',
      build: () {
        when(mockRepository.deleteAccount(any)).thenThrow(_dioError(401));
        return bloc;
      },
      act: (b) => b.add(const DeleteAccount('membre-1')),
      expect: () => [
        isA<AccountDeleting>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Non autorisé')),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [AccountDeleting, ProfileError] on DioException (403)',
      build: () {
        when(mockRepository.deleteAccount(any)).thenThrow(_dioError(403));
        return bloc;
      },
      act: (b) => b.add(const DeleteAccount('membre-1')),
      expect: () => [
        isA<AccountDeleting>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Accès refusé')),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [AccountDeleting, ProfileError] on network error',
      build: () {
        when(mockRepository.deleteAccount(any)).thenThrow(_networkError());
        return bloc;
      },
      act: (b) => b.add(const DeleteAccount('membre-1')),
      expect: () => [
        isA<AccountDeleting>(),
        isA<ProfileError>().having(
          (s) => s.message,
          'message',
          contains('Délai de connexion'),
        ),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'emits [AccountDeleting, ProfileError] on generic exception',
      build: () {
        when(mockRepository.deleteAccount(any))
            .thenThrow(Exception('Server unreachable'));
        return bloc;
      },
      act: (b) => b.add(const DeleteAccount('membre-1')),
      expect: () => [isA<AccountDeleting>(), isA<ProfileError>()],
    );
  });

  // ─── _networkErrorMessage coverage ─────────────────────────────────────────

  group('_networkErrorMessage DioExceptionType coverage', () {
    blocTest<ProfileBloc, ProfileState>(
      'sendTimeout returns Délai de connexion dépassé',
      build: () {
        when(mockGetProfile()).thenThrow(DioException(
          requestOptions: RequestOptions(path: '/'),
          type: DioExceptionType.sendTimeout,
        ));
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Délai de connexion')),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'receiveTimeout returns Délai de connexion dépassé',
      build: () {
        when(mockGetProfile()).thenThrow(DioException(
          requestOptions: RequestOptions(path: '/'),
          type: DioExceptionType.receiveTimeout,
        ));
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Délai de connexion')),
      ],
    );

    blocTest<ProfileBloc, ProfileState>(
      'unknown DioExceptionType returns Erreur réseau',
      build: () {
        when(mockGetProfile()).thenThrow(DioException(
          requestOptions: RequestOptions(path: '/'),
          type: DioExceptionType.unknown,
        ));
        return bloc;
      },
      act: (b) => b.add(const LoadMe()),
      expect: () => [
        isA<ProfileLoading>(),
        isA<ProfileError>()
            .having((s) => s.message, 'message', contains('Erreur réseau')),
      ],
    );
  });

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

  group('ProfileState equality', () {
    test('ProfileLoaded with same membre are equal', () {
      final m = _membre();
      expect(ProfileLoaded(m), equals(ProfileLoaded(m)));
    });

    test('ProfileError with same message are equal', () {
      const s1 = ProfileError('error');
      const s2 = ProfileError('error');
      expect(s1, equals(s2));
    });

    test('ProfileError with different messages are not equal', () {
      expect(const ProfileError('a'), isNot(equals(const ProfileError('b'))));
    });

    test('ProfileUpdating contains membre in props', () {
      final m = _membre();
      expect(ProfileUpdating(m).props, contains(m));
    });

    test('ProfileUpdated contains membre in props', () {
      final m = _membre();
      expect(ProfileUpdated(m).props, contains(m));
    });
  });

  // ─── ProfileEvent equality ──────────────────────────────────────────────────

  group('ProfileEvent equality', () {
    test('LoadMe events are equal', () {
      expect(const LoadMe(), equals(const LoadMe()));
    });

    test('LoadMyProfile with same email are equal', () {
      expect(
        const LoadMyProfile('a@b.com'),
        equals(const LoadMyProfile('a@b.com')),
      );
    });

    test('ChangePassword props are correct', () {
      const event = ChangePassword(
        membreId: 'm-1',
        oldPassword: 'old',
        newPassword: 'new',
      );
      expect(event.props, containsAll(['m-1', 'old', 'new']));
    });

    test('DeleteAccount props are correct', () {
      const event = DeleteAccount('m-99');
      expect(event.props, contains('m-99'));
    });
  });
}
