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/explore/presentation/bloc/network_bloc.dart';
import 'package:unionflow_mobile_apps/features/explore/presentation/bloc/network_event.dart';
import 'package:unionflow_mobile_apps/features/explore/presentation/bloc/network_state.dart';
import 'package:unionflow_mobile_apps/features/explore/data/repositories/network_repository.dart';

@GenerateMocks([NetworkRepository])
import 'network_bloc_test.mocks.dart';

void main() {
  late NetworkBloc bloc;
  late MockNetworkRepository mockRepository;

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

  NetworkItem fakeMember({
    String id = 'mbr-1',
    bool isConnected = false,
  }) =>
      NetworkItem(
        id: id,
        name: 'Jean Dupont',
        subtitle: 'Comptable',
        type: 'Member',
        isConnected: isConnected,
      );

  NetworkItem fakeOrg({String id = 'org-1'}) => NetworkItem(
        id: id,
        name: 'Association Test',
        subtitle: 'ASSOCIATION',
        type: 'Organization',
        isConnected: false,
      );

  setUp(() {
    mockRepository = MockNetworkRepository();
    bloc = NetworkBloc(mockRepository);
  });

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

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

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

  // ── LoadNetworkRequested ──────────────────────────────────────────────────

  group('LoadNetworkRequested', () {
    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkLoading, NetworkLoaded] on success with followed members',
      build: () {
        when(mockRepository.getFollowedIds())
            .thenAnswer((_) async => ['mbr-1', 'mbr-2']);
        when(mockRepository.search('', followedIds: anyNamed('followedIds')))
            .thenAnswer((_) async => [
                  fakeMember(isConnected: true),
                  fakeMember(id: 'mbr-2', isConnected: true),
                  fakeOrg(),
                ]);
        return bloc;
      },
      act: (b) => b.add(LoadNetworkRequested()),
      expect: () => [
        isA<NetworkLoading>(),
        isA<NetworkLoaded>()
            .having((s) => s.items.length, 'items.length', 3)
            .having((s) => s.currentQuery, 'currentQuery', ''),
      ],
      verify: (_) {
        verify(mockRepository.getFollowedIds()).called(1);
        verify(mockRepository.search('', followedIds: anyNamed('followedIds'))).called(1);
      },
    );

    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkLoading, NetworkLoaded] with empty list',
      build: () {
        when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
        when(mockRepository.search('', followedIds: anyNamed('followedIds')))
            .thenAnswer((_) async => []);
        return bloc;
      },
      act: (b) => b.add(LoadNetworkRequested()),
      expect: () => [
        isA<NetworkLoading>(),
        isA<NetworkLoaded>().having((s) => s.items, 'items', isEmpty),
      ],
    );

    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkLoading, NetworkError] on getFollowedIds failure',
      build: () {
        when(mockRepository.getFollowedIds()).thenThrow(Exception('auth error'));
        return bloc;
      },
      act: (b) => b.add(LoadNetworkRequested()),
      expect: () => [
        isA<NetworkLoading>(),
        isA<NetworkError>().having(
          (s) => s.message,
          'message',
          contains('Erreur'),
        ),
      ],
    );

    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkLoading, NetworkError] on search failure',
      build: () {
        when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
        when(mockRepository.search(any, followedIds: anyNamed('followedIds')))
            .thenThrow(Exception('search error'));
        return bloc;
      },
      act: (b) => b.add(LoadNetworkRequested()),
      expect: () => [isA<NetworkLoading>(), isA<NetworkError>()],
    );
  });

  // ── SearchNetworkRequested ────────────────────────────────────────────────

  group('SearchNetworkRequested', () {
    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkLoading, NetworkLoaded] with search results',
      build: () {
        when(mockRepository.getFollowedIds()).thenAnswer((_) async => ['mbr-1']);
        when(mockRepository.search('Jean', followedIds: anyNamed('followedIds')))
            .thenAnswer((_) async => [fakeMember(isConnected: true)]);
        return bloc;
      },
      act: (b) => b.add(const SearchNetworkRequested('Jean')),
      expect: () => [
        isA<NetworkLoading>(),
        isA<NetworkLoaded>()
            .having((s) => s.currentQuery, 'currentQuery', 'Jean')
            .having((s) => s.items.length, 'items.length', 1),
      ],
      verify: (_) {
        verify(mockRepository.search('Jean', followedIds: anyNamed('followedIds'))).called(1);
      },
    );

    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkLoading, NetworkLoaded] with empty query (no search)',
      build: () {
        when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
        when(mockRepository.search('', followedIds: anyNamed('followedIds')))
            .thenAnswer((_) async => [fakeMember()]);
        return bloc;
      },
      act: (b) => b.add(const SearchNetworkRequested('')),
      expect: () => [
        isA<NetworkLoading>(),
        isA<NetworkLoaded>().having((s) => s.currentQuery, 'currentQuery', ''),
      ],
    );

    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkLoading, NetworkLoaded] with whitespace-only query (treated as empty)',
      build: () {
        when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
        when(mockRepository.search('', followedIds: anyNamed('followedIds')))
            .thenAnswer((_) async => []);
        return bloc;
      },
      act: (b) => b.add(const SearchNetworkRequested('   ')),
      expect: () => [
        isA<NetworkLoading>(),
        isA<NetworkLoaded>().having((s) => s.currentQuery, 'currentQuery', ''),
      ],
    );

    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkLoading, NetworkError] on search failure',
      build: () {
        when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
        when(mockRepository.search(any, followedIds: anyNamed('followedIds')))
            .thenThrow(Exception('search failed'));
        return bloc;
      },
      act: (b) => b.add(const SearchNetworkRequested('error')),
      expect: () => [
        isA<NetworkLoading>(),
        isA<NetworkError>(),
      ],
    );
  });

  // ── ToggleFollowRequested ─────────────────────────────────────────────────

  group('ToggleFollowRequested — Member type', () {
    // Prerequisite: put bloc in NetworkLoaded state first
    final loadedState = NetworkLoaded(
      items: [
        fakeMember(id: 'mbr-1', isConnected: false),
        fakeOrg(),
      ],
      currentQuery: 'test',
    );

    blocTest<NetworkBloc, NetworkState>(
      'follows an unfollowed Member and emits NetworkLoaded with isConnected=true',
      build: () {
        when(mockRepository.follow('mbr-1')).thenAnswer((_) async => true);
        return bloc;
      },
      seed: () => loadedState,
      act: (b) => b.add(const ToggleFollowRequested('mbr-1')),
      expect: () => [
        isA<NetworkLoaded>().having(
          (s) => s.items.firstWhere((i) => i.id == 'mbr-1').isConnected,
          'mbr-1.isConnected',
          true,
        ),
      ],
      verify: (_) => verify(mockRepository.follow('mbr-1')).called(1),
    );

    blocTest<NetworkBloc, NetworkState>(
      'unfollows a followed Member and emits NetworkLoaded with isConnected=false',
      build: () {
        when(mockRepository.unfollow('mbr-1')).thenAnswer((_) async => false);
        return bloc;
      },
      seed: () => NetworkLoaded(
        items: [fakeMember(id: 'mbr-1', isConnected: true)],
        currentQuery: '',
      ),
      act: (b) => b.add(const ToggleFollowRequested('mbr-1')),
      expect: () => [
        isA<NetworkLoaded>().having(
          (s) => s.items.first.isConnected,
          'isConnected',
          false,
        ),
      ],
      verify: (_) => verify(mockRepository.unfollow('mbr-1')).called(1),
    );

    blocTest<NetworkBloc, NetworkState>(
      'emits [NetworkError] when follow call fails',
      build: () {
        when(mockRepository.follow(any)).thenThrow(Exception('follow failed'));
        return bloc;
      },
      seed: () => loadedState,
      act: (b) => b.add(const ToggleFollowRequested('mbr-1')),
      expect: () => [isA<NetworkError>()],
    );
  });

  group('ToggleFollowRequested — Organization type (local toggle only)', () {
    blocTest<NetworkBloc, NetworkState>(
      'toggles Organization follow locally without calling repository',
      build: () => bloc,
      seed: () => NetworkLoaded(
        items: [fakeOrg(id: 'org-1')],
        currentQuery: '',
      ),
      act: (b) => b.add(const ToggleFollowRequested('org-1')),
      expect: () => [
        isA<NetworkLoaded>().having(
          (s) => s.items.first.isConnected,
          'isConnected',
          true,
        ),
      ],
      verify: (_) {
        verifyNever(mockRepository.follow(any));
        verifyNever(mockRepository.unfollow(any));
      },
    );
  });

  group('ToggleFollowRequested — guard: item not in list', () {
    blocTest<NetworkBloc, NetworkState>(
      'does nothing when item id not found in current list',
      build: () => bloc,
      seed: () => NetworkLoaded(
        items: [fakeMember(id: 'mbr-1')],
        currentQuery: '',
      ),
      act: (b) => b.add(const ToggleFollowRequested('non-existent')),
      expect: () => [],
    );

    blocTest<NetworkBloc, NetworkState>(
      'does nothing when state is not NetworkLoaded',
      build: () => bloc,
      // Default initial state (NetworkInitial)
      act: (b) => b.add(const ToggleFollowRequested('mbr-1')),
      expect: () => [],
    );
  });
}
