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/core/config/environment.dart';
import 'package:unionflow_mobile_apps/features/notifications/data/repositories/notification_feed_repository.dart';
import 'package:unionflow_mobile_apps/features/notifications/presentation/bloc/notification_bloc.dart';
import 'package:unionflow_mobile_apps/features/notifications/presentation/bloc/notification_event.dart';
import 'package:unionflow_mobile_apps/features/notifications/presentation/bloc/notification_state.dart';

@GenerateMocks([NotificationFeedRepository])
import 'notification_bloc_test.mocks.dart';

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

NotificationItem _makeItem({
  String id = 'item-1',
  String title = 'Test Notification',
  String body = 'Corps du message',
  bool isRead = false,
  String category = 'system',
}) =>
    NotificationItem(
      id: id,
      title: title,
      body: body,
      date: DateTime(2026, 4, 20),
      isRead: isRead,
      category: category,
    );

DioException _cancelException() => DioException(
      requestOptions: RequestOptions(path: '/notifications'),
      type: DioExceptionType.cancel,
    );

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

void main() {
  late MockNotificationFeedRepository mockRepository;

  setUpAll(() {
    try {
      AppConfig.initialize();
    } catch (_) {
      // Already initialized.
    }
  });

  NotificationBloc buildBloc() => NotificationBloc(mockRepository);

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

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

  group('initial state', () {
    test('is NotificationInitial', () {
      expect(buildBloc().state, isA<NotificationInitial>());
    });
  });

  // ─── LoadNotificationsRequested ───────────────────────────────────────────

  group('LoadNotificationsRequested', () {
    final items = [
      _makeItem(),
      _makeItem(id: 'item-2', title: 'Finance Alert', category: 'finance'),
      _makeItem(id: 'item-3', title: 'Event', category: 'event', isRead: true),
    ];

    blocTest<NotificationBloc, NotificationState>(
      'emits [Loading, Loaded] with items on success',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.getNotifications()).thenAnswer((_) async => items),
      act: (b) => b.add(LoadNotificationsRequested()),
      expect: () => [
        isA<NotificationLoading>(),
        isA<NotificationLoaded>()
            .having((s) => s.items.length, 'count', 3)
            .having((s) => s.items.first.id, 'first id', 'item-1'),
      ],
    );

    blocTest<NotificationBloc, NotificationState>(
      'emits [Loading, Loaded(empty)] when repository returns empty list',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.getNotifications()).thenAnswer((_) async => []),
      act: (b) => b.add(LoadNotificationsRequested()),
      expect: () => [
        isA<NotificationLoading>(),
        isA<NotificationLoaded>().having((s) => s.items, 'items', isEmpty),
      ],
    );

    blocTest<NotificationBloc, NotificationState>(
      'emits [Loading, Error] on generic exception',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.getNotifications()).thenThrow(Exception('network')),
      act: (b) => b.add(LoadNotificationsRequested()),
      expect: () => [
        isA<NotificationLoading>(),
        isA<NotificationError>()
            .having((e) => e.message, 'message', contains('chargement')),
      ],
    );

    blocTest<NotificationBloc, NotificationState>(
      'emits [Loading, Error] on DioException (non-cancel)',
      build: buildBloc,
      setUp: () => when(mockRepository.getNotifications()).thenThrow(
        DioException(
          requestOptions: RequestOptions(path: '/notifications'),
          type: DioExceptionType.connectionError,
        ),
      ),
      act: (b) => b.add(LoadNotificationsRequested()),
      expect: () => [
        isA<NotificationLoading>(),
        isA<NotificationError>(),
      ],
    );

    blocTest<NotificationBloc, NotificationState>(
      'swallows DioException.cancel — no error emitted',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.getNotifications()).thenThrow(_cancelException()),
      act: (b) => b.add(LoadNotificationsRequested()),
      expect: () => [isA<NotificationLoading>()],
    );

    blocTest<NotificationBloc, NotificationState>(
      'calls getNotifications exactly once',
      build: buildBloc,
      setUp: () =>
          when(mockRepository.getNotifications()).thenAnswer((_) async => items),
      act: (b) => b.add(LoadNotificationsRequested()),
      verify: (_) => verify(mockRepository.getNotifications()).called(1),
    );
  });

  // ─── NotificationMarkedAsRead ─────────────────────────────────────────────

  group('NotificationMarkedAsRead', () {
    final items = [
      _makeItem(id: 'item-1', isRead: false),
      _makeItem(id: 'item-2', isRead: false),
      _makeItem(id: 'item-3', isRead: true),
    ];
    final loadedState = NotificationLoaded(items: items);

    blocTest<NotificationBloc, NotificationState>(
      'emits Loaded with target item marked as read',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockRepository.markAsRead('item-1')).thenAnswer((_) async {}),
      act: (b) => b.add(const NotificationMarkedAsRead('item-1')),
      expect: () => [
        isA<NotificationLoaded>()
            .having(
              (s) => s.items.firstWhere((i) => i.id == 'item-1').isRead,
              'item-1 isRead',
              true,
            )
            .having(
              (s) => s.items.firstWhere((i) => i.id == 'item-2').isRead,
              'item-2 unchanged',
              false,
            ),
      ],
    );

    blocTest<NotificationBloc, NotificationState>(
      'preserves all other item properties after marking read',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockRepository.markAsRead('item-1')).thenAnswer((_) async {}),
      act: (b) => b.add(const NotificationMarkedAsRead('item-1')),
      expect: () => [
        isA<NotificationLoaded>().having(
          (s) => s.items.firstWhere((i) => i.id == 'item-1').title,
          'title preserved',
          'Test Notification',
        ),
      ],
    );

    blocTest<NotificationBloc, NotificationState>(
      'no re-emission when id not found (items unchanged, equatable dedup)',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockRepository.markAsRead('unknown')).thenAnswer((_) async {}),
      act: (b) => b.add(const NotificationMarkedAsRead('unknown')),
      // The map returns the same object references for non-matching items,
      // resulting in an identical NotificationLoaded state — no re-emission.
      expect: () => [],
    );

    blocTest<NotificationBloc, NotificationState>(
      'does nothing when state is not NotificationLoaded',
      build: buildBloc,
      // state is NotificationInitial
      act: (b) => b.add(const NotificationMarkedAsRead('item-1')),
      expect: () => [],
    );

    blocTest<NotificationBloc, NotificationState>(
      'does nothing when state is NotificationLoading',
      build: buildBloc,
      seed: () => NotificationLoading(),
      act: (b) => b.add(const NotificationMarkedAsRead('item-1')),
      expect: () => [],
    );

    blocTest<NotificationBloc, NotificationState>(
      'emits Error when markAsRead throws',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockRepository.markAsRead(any)).thenThrow(Exception('server error')),
      act: (b) => b.add(const NotificationMarkedAsRead('item-1')),
      expect: () => [
        isA<NotificationError>()
            .having((e) => e.message, 'message', contains('marquer')),
      ],
    );

    blocTest<NotificationBloc, NotificationState>(
      'swallows DioException.cancel in markAsRead',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockRepository.markAsRead(any)).thenThrow(_cancelException()),
      act: (b) => b.add(const NotificationMarkedAsRead('item-1')),
      expect: () => [],
    );

    blocTest<NotificationBloc, NotificationState>(
      'calls markAsRead with correct id',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockRepository.markAsRead('item-2')).thenAnswer((_) async {}),
      act: (b) => b.add(const NotificationMarkedAsRead('item-2')),
      verify: (_) => verify(mockRepository.markAsRead('item-2')).called(1),
    );

    blocTest<NotificationBloc, NotificationState>(
      'marking already-read item produces same state (no re-emission)',
      build: buildBloc,
      seed: () => loadedState,
      setUp: () =>
          when(mockRepository.markAsRead('item-3')).thenAnswer((_) async {}),
      act: (b) => b.add(const NotificationMarkedAsRead('item-3')),
      // Re-marking an already-read item rebuilds NotificationLoaded with
      // identical items (isRead=true stays true). Equatable dedup prevents
      // re-emission because the resulting state equals the current state.
      expect: () => [],
    );
  });

  // ─── Combined flows ───────────────────────────────────────────────────────

  group('combined flows', () {
    final items = [
      _makeItem(id: 'a', isRead: false),
      _makeItem(id: 'b', isRead: false),
    ];

    blocTest<NotificationBloc, NotificationState>(
      'load then mark as read produces correct final state',
      build: buildBloc,
      setUp: () {
        when(mockRepository.getNotifications()).thenAnswer((_) async => items);
        when(mockRepository.markAsRead('a')).thenAnswer((_) async {});
      },
      act: (b) async {
        b.add(LoadNotificationsRequested());
        await Future.delayed(Duration.zero);
        b.add(const NotificationMarkedAsRead('a'));
      },
      expect: () => [
        isA<NotificationLoading>(),
        isA<NotificationLoaded>().having((s) => s.items.length, 'count', 2),
        isA<NotificationLoaded>().having(
          (s) => s.items.firstWhere((i) => i.id == 'a').isRead,
          'a is read',
          true,
        ),
      ],
    );

    blocTest<NotificationBloc, NotificationState>(
      'reload after mark as read reflects fresh data',
      build: buildBloc,
      setUp: () {
        final updated = [
          _makeItem(id: 'a', isRead: true),
          _makeItem(id: 'b', isRead: false),
        ];
        when(mockRepository.getNotifications())
            .thenAnswer((_) async => updated);
      },
      seed: () => NotificationLoaded(items: items),
      act: (b) => b.add(LoadNotificationsRequested()),
      expect: () => [
        isA<NotificationLoading>(),
        isA<NotificationLoaded>().having(
          (s) => s.items.firstWhere((i) => i.id == 'a').isRead,
          'a is read after reload',
          true,
        ),
      ],
    );
  });

  // ─── NotificationItem equality ────────────────────────────────────────────

  group('NotificationItem', () {
    test('two items with same data are equal', () {
      final a = _makeItem();
      final b = _makeItem();
      expect(a, b);
    });

    test('items with different isRead are not equal', () {
      final a = _makeItem(isRead: false);
      final b = _makeItem(isRead: true);
      expect(a, isNot(b));
    });

    test('items with different category are not equal', () {
      final a = _makeItem(category: 'system');
      final b = _makeItem(category: 'finance');
      expect(a, isNot(b));
    });
  });

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

  group('State equality', () {
    final items = [_makeItem()];

    test('NotificationLoaded with same items are equal', () {
      expect(NotificationLoaded(items: items), NotificationLoaded(items: items));
    });

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