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/feed/presentation/bloc/unified_feed_bloc.dart';
import 'package:unionflow_mobile_apps/features/feed/presentation/bloc/unified_feed_event.dart';
import 'package:unionflow_mobile_apps/features/feed/presentation/bloc/unified_feed_state.dart';
import 'package:unionflow_mobile_apps/features/feed/data/repositories/feed_repository.dart';
import 'package:unionflow_mobile_apps/features/feed/domain/entities/feed_item.dart';

@GenerateMocks([FeedRepository])
import 'unified_feed_bloc_test.mocks.dart';

void main() {
  late UnifiedFeedBloc bloc;
  late MockFeedRepository mockRepository;

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

  FeedItem fakeFeedItem({
    String id = 'item-1',
    bool isLikedByMe = false,
    int likesCount = 5,
  }) =>
      FeedItem(
        id: id,
        type: FeedItemType.post,
        authorName: 'Jean Dupont',
        createdAt: DateTime(2026, 1, 1),
        content: 'Post content $id',
        likesCount: likesCount,
        commentsCount: 2,
        isLikedByMe: isLikedByMe,
      );

  List<FeedItem> fakeFeedPage({int count = 10, int startIndex = 0}) =>
      List.generate(count, (i) => fakeFeedItem(id: 'item-${startIndex + i}'));

  setUp(() {
    mockRepository = MockFeedRepository();
    bloc = UnifiedFeedBloc(mockRepository);
  });

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

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

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

  // ── LoadFeedRequested ─────────────────────────────────────────────────────

  group('LoadFeedRequested', () {
    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'emits [UnifiedFeedLoading, UnifiedFeedLoaded] on success with 10 items',
      build: () {
        when(mockRepository.getFeed(page: 0, size: 10))
            .thenAnswer((_) async => fakeFeedPage());
        return bloc;
      },
      act: (b) => b.add(const LoadFeedRequested()),
      expect: () => [
        isA<UnifiedFeedLoading>(),
        isA<UnifiedFeedLoaded>()
            .having((s) => s.items.length, 'items.length', 10)
            .having((s) => s.hasReachedMax, 'hasReachedMax', false),
      ],
      verify: (_) => verify(mockRepository.getFeed(page: 0, size: 10)).called(1),
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'emits [UnifiedFeedLoading, UnifiedFeedLoaded] with hasReachedMax=true when fewer than 10 items',
      build: () {
        when(mockRepository.getFeed(page: 0, size: 10))
            .thenAnswer((_) async => fakeFeedPage(count: 5));
        return bloc;
      },
      act: (b) => b.add(const LoadFeedRequested()),
      expect: () => [
        isA<UnifiedFeedLoading>(),
        isA<UnifiedFeedLoaded>()
            .having((s) => s.items.length, 'items.length', 5)
            .having((s) => s.hasReachedMax, 'hasReachedMax', true),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'emits [UnifiedFeedLoading, UnifiedFeedLoaded] with empty feed',
      build: () {
        when(mockRepository.getFeed(page: 0, size: 10))
            .thenAnswer((_) async => []);
        return bloc;
      },
      act: (b) => b.add(const LoadFeedRequested()),
      expect: () => [
        isA<UnifiedFeedLoading>(),
        isA<UnifiedFeedLoaded>()
            .having((s) => s.items, 'items', isEmpty)
            .having((s) => s.hasReachedMax, 'hasReachedMax', true),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'emits [UnifiedFeedError] on repository failure',
      build: () {
        when(mockRepository.getFeed(page: 0, size: 10))
            .thenThrow(Exception('network error'));
        return bloc;
      },
      act: (b) => b.add(const LoadFeedRequested()),
      expect: () => [
        isA<UnifiedFeedLoading>(),
        isA<UnifiedFeedError>().having(
          (s) => s.message,
          'message',
          contains('Erreur de chargement'),
        ),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'does NOT emit UnifiedFeedLoading when isRefresh=true',
      build: () {
        when(mockRepository.getFeed(page: 0, size: 10))
            .thenAnswer((_) async => fakeFeedPage(count: 3));
        return bloc;
      },
      act: (b) => b.add(const LoadFeedRequested(isRefresh: true)),
      expect: () => [
        // No UnifiedFeedLoading emitted for refresh
        isA<UnifiedFeedLoaded>(),
      ],
    );
  });

  // ── FeedLoadMoreRequested ─────────────────────────────────────────────────

  group('FeedLoadMoreRequested', () {
    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'emits updated UnifiedFeedLoaded with concatenated items on success',
      build: () {
        when(mockRepository.getFeed(page: 1, size: 10))
            .thenAnswer((_) async => fakeFeedPage(count: 10, startIndex: 10));
        return bloc;
      },
      seed: () => UnifiedFeedLoaded(
        items: fakeFeedPage(),
        hasReachedMax: false,
      ),
      act: (b) => b.add(FeedLoadMoreRequested()),
      expect: () => [
        // First: isFetchingMore=true
        isA<UnifiedFeedLoaded>().having(
          (s) => s.isFetchingMore,
          'isFetchingMore',
          true,
        ),
        // Then: items appended, isFetchingMore=false
        isA<UnifiedFeedLoaded>()
            .having((s) => s.items.length, 'items.length', 20)
            .having((s) => s.isFetchingMore, 'isFetchingMore', false)
            .having((s) => s.hasReachedMax, 'hasReachedMax', false),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'emits UnifiedFeedLoaded with hasReachedMax=true when no more items',
      build: () {
        when(mockRepository.getFeed(page: 1, size: 10))
            .thenAnswer((_) async => []);
        return bloc;
      },
      seed: () => UnifiedFeedLoaded(
        items: fakeFeedPage(),
        hasReachedMax: false,
      ),
      act: (b) => b.add(FeedLoadMoreRequested()),
      expect: () => [
        isA<UnifiedFeedLoaded>().having((s) => s.isFetchingMore, 'isFetchingMore', true),
        isA<UnifiedFeedLoaded>()
            .having((s) => s.hasReachedMax, 'hasReachedMax', true)
            .having((s) => s.items.length, 'items.length', 10),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'emits loadMoreErrorMessage on load more failure',
      build: () {
        when(mockRepository.getFeed(page: 1, size: 10))
            .thenThrow(Exception('page load error'));
        return bloc;
      },
      seed: () => UnifiedFeedLoaded(
        items: fakeFeedPage(),
        hasReachedMax: false,
      ),
      act: (b) => b.add(FeedLoadMoreRequested()),
      expect: () => [
        isA<UnifiedFeedLoaded>().having((s) => s.isFetchingMore, 'isFetchingMore', true),
        isA<UnifiedFeedLoaded>()
            .having((s) => s.isFetchingMore, 'isFetchingMore', false)
            .having(
              (s) => s.loadMoreErrorMessage,
              'loadMoreErrorMessage',
              isNotNull,
            ),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'does nothing when hasReachedMax is true',
      build: () => bloc,
      seed: () => UnifiedFeedLoaded(
        items: fakeFeedPage(count: 5),
        hasReachedMax: true,
      ),
      act: (b) => b.add(FeedLoadMoreRequested()),
      expect: () => [],
      verify: (_) => verifyNever(mockRepository.getFeed(
        page: anyNamed('page'),
        size: anyNamed('size'),
      )),
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'does nothing when isFetchingMore is already true',
      build: () => bloc,
      seed: () => UnifiedFeedLoaded(
        items: fakeFeedPage(),
        hasReachedMax: false,
        isFetchingMore: true,
      ),
      act: (b) => b.add(FeedLoadMoreRequested()),
      expect: () => [],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'does nothing when state is not UnifiedFeedLoaded',
      build: () => bloc,
      // Default state: UnifiedFeedInitial
      act: (b) => b.add(FeedLoadMoreRequested()),
      expect: () => [],
    );
  });

  // ── ClearLoadMoreError ────────────────────────────────────────────────────

  group('ClearLoadMoreError', () {
    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'clears loadMoreErrorMessage from current loaded state',
      build: () => bloc,
      seed: () => UnifiedFeedLoaded(
        items: fakeFeedPage(count: 5),
        loadMoreErrorMessage: 'Impossible de charger plus',
      ),
      act: (b) => b.add(ClearLoadMoreError()),
      expect: () => [
        isA<UnifiedFeedLoaded>().having(
          (s) => s.loadMoreErrorMessage,
          'loadMoreErrorMessage',
          isNull,
        ),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'does nothing when state is not UnifiedFeedLoaded',
      build: () => bloc,
      act: (b) => b.add(ClearLoadMoreError()),
      expect: () => [],
    );
  });

  // ── FeedItemLiked ─────────────────────────────────────────────────────────

  group('FeedItemLiked', () {
    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'toggles like on an item (not liked → liked, likesCount +1)',
      build: () => bloc,
      seed: () => UnifiedFeedLoaded(
        items: [
          fakeFeedItem(id: 'item-1', isLikedByMe: false, likesCount: 5),
          fakeFeedItem(id: 'item-2', isLikedByMe: true, likesCount: 3),
        ],
      ),
      act: (b) => b.add(const FeedItemLiked('item-1')),
      expect: () => [
        isA<UnifiedFeedLoaded>().having(
          (s) => s.items.firstWhere((i) => i.id == 'item-1').isLikedByMe,
          'item-1.isLikedByMe',
          true,
        ),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'toggles like count correctly (liked → unliked, likesCount -1)',
      build: () => bloc,
      seed: () => UnifiedFeedLoaded(
        items: [
          fakeFeedItem(id: 'item-1', isLikedByMe: true, likesCount: 5),
        ],
      ),
      act: (b) => b.add(const FeedItemLiked('item-1')),
      expect: () => [
        isA<UnifiedFeedLoaded>()
            .having(
              (s) => s.items.first.isLikedByMe,
              'isLikedByMe',
              false,
            )
            .having(
              (s) => s.items.first.likesCount,
              'likesCount',
              4,
            ),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'increments likesCount when liking item',
      build: () => bloc,
      seed: () => UnifiedFeedLoaded(
        items: [
          fakeFeedItem(id: 'item-1', isLikedByMe: false, likesCount: 10),
        ],
      ),
      act: (b) => b.add(const FeedItemLiked('item-1')),
      expect: () => [
        isA<UnifiedFeedLoaded>().having(
          (s) => s.items.first.likesCount,
          'likesCount',
          11,
        ),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'does not modify other items when liking one',
      build: () => bloc,
      seed: () => UnifiedFeedLoaded(
        items: [
          fakeFeedItem(id: 'item-1', isLikedByMe: false, likesCount: 5),
          fakeFeedItem(id: 'item-2', isLikedByMe: true, likesCount: 3),
        ],
      ),
      act: (b) => b.add(const FeedItemLiked('item-1')),
      expect: () => [
        isA<UnifiedFeedLoaded>().having(
          (s) => s.items.firstWhere((i) => i.id == 'item-2').isLikedByMe,
          'item-2.isLikedByMe stays true',
          true,
        ),
      ],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'does nothing when state is not UnifiedFeedLoaded',
      build: () => bloc,
      // Default: UnifiedFeedInitial
      act: (b) => b.add(const FeedItemLiked('item-1')),
      expect: () => [],
    );

    blocTest<UnifiedFeedBloc, UnifiedFeedState>(
      'does nothing when item id not found in list',
      build: () => bloc,
      seed: () => UnifiedFeedLoaded(
        items: [fakeFeedItem(id: 'item-1')],
      ),
      act: (b) => b.add(const FeedItemLiked('non-existent')),
      expect: () => [
        // State is re-emitted unchanged (items mapped, no match found)
        isA<UnifiedFeedLoaded>().having(
          (s) => s.items.first.id,
          'items[0].id',
          'item-1',
        ),
      ],
    );
  });

  // ── UnifiedFeedLoaded.copyWith ────────────────────────────────────────────

  group('UnifiedFeedLoaded.copyWith', () {
    test('preserves existing values when not overridden', () {
      final state = UnifiedFeedLoaded(
        items: fakeFeedPage(count: 3),
        hasReachedMax: false,
        isFetchingMore: false,
        loadMoreErrorMessage: 'some error',
      );
      final copy = state.copyWith(hasReachedMax: true);
      expect(copy.hasReachedMax, true);
      expect(copy.items.length, 3);
      // Note: copyWith always resets isFetchingMore to false and
      // loadMoreErrorMessage to null when not provided (by design)
    });

    test('loadMoreErrorMessage is null when not passed to copyWith', () {
      final state = UnifiedFeedLoaded(
        items: [],
        loadMoreErrorMessage: 'error',
      );
      final copy = state.copyWith(hasReachedMax: true);
      expect(copy.loadMoreErrorMessage, isNull);
    });
  });
}
