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/core/network/api_client.dart';
import 'package:unionflow_mobile_apps/core/network/org_context_service.dart';
import 'package:unionflow_mobile_apps/features/organizations/bloc/org_switcher_bloc.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/org_switcher_entry.dart';

@GenerateMocks([ApiClient, OrgContextService])
import 'org_switcher_bloc_test.mocks.dart';

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

OrgSwitcherEntry _makeEntry({
  String id = 'org-1',
  String nom = 'Association Alpha',
  String type = 'ASSOCIATION',
  String? modules,
}) =>
    OrgSwitcherEntry(
      organisationId: id,
      nom: nom,
      typeOrganisation: type,
      modulesActifs: modules,
    );

Response<List<dynamic>> _mockResponse(List<Map<String, dynamic>> data) =>
    Response(
      requestOptions: RequestOptions(path: '/api/membres/mes-organisations'),
      statusCode: 200,
      data: data,
    );

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

void main() {
  late MockApiClient mockApiClient;
  late MockOrgContextService mockOrgContextService;

  setUpAll(() {
    // AppConfig.initialize() uses `late final` fields — only call once.
    try {
      AppConfig.initialize();
    } catch (_) {
      // Already initialized in a previous test run within the same process.
    }
  });

  OrgSwitcherBloc buildBloc() =>
      OrgSwitcherBloc(mockApiClient, mockOrgContextService);

  setUp(() {
    mockApiClient = MockApiClient();
    mockOrgContextService = MockOrgContextService();

    // Default: no existing context
    when(mockOrgContextService.hasContext).thenReturn(false);
    when(mockOrgContextService.activeOrganisationId).thenReturn(null);
    when(mockOrgContextService.setActiveOrganisation(
      organisationId: anyNamed('organisationId'),
      nom: anyNamed('nom'),
      type: anyNamed('type'),
      modulesActifsCsv: anyNamed('modulesActifsCsv'),
    )).thenReturn(null);
  });

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

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

  // ─── OrgSwitcherLoadRequested ─────────────────────────────────────────────

  group('OrgSwitcherLoadRequested', () {
    final entry1 = _makeEntry();
    final entry2 = _makeEntry(id: 'org-2', nom: 'Tontine Beta', type: 'TONTINE');

    final rawData = [
      {
        'organisationId': 'org-1',
        'nom': 'Association Alpha',
        'typeOrganisation': 'ASSOCIATION',
      },
      {
        'organisationId': 'org-2',
        'nom': 'Tontine Beta',
        'typeOrganisation': 'TONTINE',
      },
    ];

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'emits [Loading, Loaded] with first org as active when no context',
      build: buildBloc,
      setUp: () {
        when(mockApiClient.get<List<dynamic>>(any))
            .thenAnswer((_) async => _mockResponse(rawData));
      },
      act: (b) => b.add(const OrgSwitcherLoadRequested()),
      expect: () => [
        isA<OrgSwitcherLoading>(),
        isA<OrgSwitcherLoaded>()
            .having((s) => s.organisations.length, 'count', 2)
            .having((s) => s.active?.organisationId, 'active', 'org-1'),
      ],
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'calls setActiveOrganisation for first org when no existing context',
      build: buildBloc,
      setUp: () {
        when(mockApiClient.get<List<dynamic>>(any))
            .thenAnswer((_) async => _mockResponse(rawData));
      },
      act: (b) => b.add(const OrgSwitcherLoadRequested()),
      verify: (_) {
        verify(mockOrgContextService.setActiveOrganisation(
          organisationId: 'org-1',
          nom: 'Association Alpha',
          type: anyNamed('type'),
          modulesActifsCsv: anyNamed('modulesActifsCsv'),
        )).called(1);
      },
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'restores previously active org when context already set',
      build: buildBloc,
      setUp: () {
        when(mockOrgContextService.hasContext).thenReturn(true);
        when(mockOrgContextService.activeOrganisationId).thenReturn('org-2');
        when(mockApiClient.get<List<dynamic>>(any))
            .thenAnswer((_) async => _mockResponse(rawData));
      },
      act: (b) => b.add(const OrgSwitcherLoadRequested()),
      expect: () => [
        isA<OrgSwitcherLoading>(),
        isA<OrgSwitcherLoaded>()
            .having((s) => s.active?.organisationId, 'active', 'org-2'),
      ],
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'emits [Loading, Loaded] with null active when list is empty',
      build: buildBloc,
      setUp: () {
        when(mockApiClient.get<List<dynamic>>(any))
            .thenAnswer((_) async => _mockResponse([]));
      },
      act: (b) => b.add(const OrgSwitcherLoadRequested()),
      expect: () => [
        isA<OrgSwitcherLoading>(),
        isA<OrgSwitcherLoaded>()
            .having((s) => s.organisations, 'orgs', isEmpty)
            .having((s) => s.active, 'active', isNull),
      ],
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'emits [Loading, Error] on DioException',
      build: buildBloc,
      setUp: () {
        when(mockApiClient.get<List<dynamic>>(any)).thenThrow(DioException(
          requestOptions: RequestOptions(path: '/api/membres/mes-organisations'),
          message: 'connection refused',
        ));
      },
      act: (b) => b.add(const OrgSwitcherLoadRequested()),
      expect: () => [
        isA<OrgSwitcherLoading>(),
        isA<OrgSwitcherError>()
            .having((e) => e.message, 'message', contains('organisations')),
      ],
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'emits [Loading, Error] on unexpected exception',
      build: buildBloc,
      setUp: () {
        when(mockApiClient.get<List<dynamic>>(any))
            .thenThrow(Exception('unexpected'));
      },
      act: (b) => b.add(const OrgSwitcherLoadRequested()),
      expect: () => [
        isA<OrgSwitcherLoading>(),
        isA<OrgSwitcherError>()
            .having((e) => e.message, 'message', contains('inattendue')),
      ],
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'handles null data gracefully (empty list)',
      build: buildBloc,
      setUp: () {
        when(mockApiClient.get<List<dynamic>>(any)).thenAnswer((_) async =>
            Response(
              requestOptions:
                  RequestOptions(path: '/api/membres/mes-organisations'),
              statusCode: 200,
              data: null,
            ));
      },
      act: (b) => b.add(const OrgSwitcherLoadRequested()),
      expect: () => [
        isA<OrgSwitcherLoading>(),
        isA<OrgSwitcherLoaded>().having((s) => s.organisations, 'orgs', isEmpty),
      ],
    );
  });

  // ─── OrgSwitcherSelectRequested ───────────────────────────────────────────

  group('OrgSwitcherSelectRequested', () {
    final entry1 = _makeEntry();
    final entry2 = _makeEntry(id: 'org-2', nom: 'Tontine Beta', type: 'TONTINE');

    final loadedState = OrgSwitcherLoaded(
      organisations: [entry1, entry2],
      active: entry1,
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'emits Loaded with updated active entry',
      build: buildBloc,
      seed: () => loadedState,
      act: (b) => b.add(OrgSwitcherSelectRequested(entry2)),
      expect: () => [
        isA<OrgSwitcherLoaded>()
            .having((s) => s.active?.organisationId, 'active', 'org-2')
            .having((s) => s.organisations.length, 'count', 2),
      ],
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'calls setActiveOrganisation with selected org data',
      build: buildBloc,
      seed: () => loadedState,
      act: (b) => b.add(OrgSwitcherSelectRequested(entry2)),
      verify: (_) {
        verify(mockOrgContextService.setActiveOrganisation(
          organisationId: 'org-2',
          nom: 'Tontine Beta',
          type: anyNamed('type'),
          modulesActifsCsv: anyNamed('modulesActifsCsv'),
        )).called(1);
      },
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'does nothing when state is not OrgSwitcherLoaded',
      build: buildBloc,
      act: (b) => b.add(OrgSwitcherSelectRequested(entry1)),
      expect: () => [],
    );

    blocTest<OrgSwitcherBloc, OrgSwitcherState>(
      'selecting same org does not re-emit (equatable deduplication)',
      build: buildBloc,
      seed: () => loadedState,
      act: (b) => b.add(OrgSwitcherSelectRequested(entry1)),
      // copyWith(active: entry1) produces the same state as loadedState,
      // so BLoC's equatable check prevents re-emission.
      expect: () => [],
    );
  });

  // ─── OrgSwitcherEntry helpers ─────────────────────────────────────────────

  group('OrgSwitcherEntry helpers', () {
    test('libelleCourt returns nomCourt when set', () {
      final entry = OrgSwitcherEntry(
        organisationId: 'id',
        nom: 'Long Nom',
        nomCourt: 'LN',
        typeOrganisation: 'ASSO',
      );
      expect(entry.libelleCourt, 'LN');
    });

    test('libelleCourt truncates long nom', () {
      final entry = OrgSwitcherEntry(
        organisationId: 'id',
        nom: 'Un très long nom d\'organisation qui dépasse 25 caractères',
        typeOrganisation: 'ASSO',
      );
      expect(entry.libelleCourt.length, lessThanOrEqualTo(25));
      expect(entry.libelleCourt, endsWith('…'));
    });

    test('modulesActifsSet parses CSV correctly', () {
      final entry = _makeEntry(modules: 'FINANCE,EVENEMENTS, MEMBRES');
      expect(entry.modulesActifsSet, {'FINANCE', 'EVENEMENTS', 'MEMBRES'});
    });

    test('modulesActifsSet returns empty set when null', () {
      final entry = _makeEntry(modules: null);
      expect(entry.modulesActifsSet, isEmpty);
    });
  });

  // ─── OrgSwitcherLoaded.copyWith ───────────────────────────────────────────

  group('OrgSwitcherLoaded.copyWith', () {
    final entry1 = _makeEntry();
    final entry2 = _makeEntry(id: 'org-2', nom: 'Beta');

    test('updates active only', () {
      final state = OrgSwitcherLoaded(organisations: [entry1, entry2], active: entry1);
      final updated = state.copyWith(active: entry2);
      expect(updated.active, entry2);
      expect(updated.organisations, [entry1, entry2]);
    });

    test('updates organisations only', () {
      final state = OrgSwitcherLoaded(organisations: [entry1], active: entry1);
      final updated = state.copyWith(organisations: [entry1, entry2]);
      expect(updated.organisations.length, 2);
      expect(updated.active, entry1);
    });
  });
}
