/// Tests unitaires pour EvenementsBloc
library evenements_bloc_test;

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/events/bloc/evenements_bloc.dart';
import 'package:unionflow_mobile_apps/features/events/bloc/evenements_event.dart';
import 'package:unionflow_mobile_apps/features/events/bloc/evenements_state.dart';
import 'package:unionflow_mobile_apps/features/events/data/models/evenement_model.dart';
import 'package:unionflow_mobile_apps/features/events/data/repositories/evenement_repository_impl.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_events.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_event_by_id.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/create_event.dart' as uc;
import 'package:unionflow_mobile_apps/features/events/domain/usecases/update_event.dart' as uc;
import 'package:unionflow_mobile_apps/features/events/domain/usecases/delete_event.dart' as uc;
import 'package:unionflow_mobile_apps/features/events/domain/usecases/register_for_event.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/cancel_registration.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_event_participants.dart';

@GenerateMocks([
  GetEvents,
  GetEventById,
  uc.CreateEvent,
  uc.UpdateEvent,
  uc.DeleteEvent,
  RegisterForEvent,
  CancelRegistration,
  GetEventParticipants,
  IEvenementRepository,
])
import 'evenements_bloc_test.mocks.dart';

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

final _now = DateTime(2025, 6, 15);
final _later = DateTime(2025, 6, 16);

EvenementModel _makeEvenement({int id = 1, String titre = 'Réunion mensuelle'}) =>
    EvenementModel(
      id: id,
      titre: titre,
      dateDebut: _now,
      dateFin: _later,
    );

EvenementSearchResult _makeSearchResult(List<EvenementModel> items) =>
    EvenementSearchResult(
      evenements: items,
      total: items.length,
      page: 0,
      size: 20,
      totalPages: items.isEmpty ? 0 : 1,
    );

DioException _makeDioException(int statusCode) => DioException(
      requestOptions: RequestOptions(path: '/test'),
      response: Response(
        requestOptions: RequestOptions(path: '/test'),
        statusCode: statusCode,
      ),
      type: DioExceptionType.badResponse,
    );

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

void main() {
  late MockGetEvents mockGetEvents;
  late MockGetEventById mockGetEventById;
  late MockCreateEvent mockCreateEvent;
  late MockUpdateEvent mockUpdateEvent;
  late MockDeleteEvent mockDeleteEvent;
  late MockRegisterForEvent mockRegisterForEvent;
  late MockCancelRegistration mockCancelRegistration;
  late MockGetEventParticipants mockGetEventParticipants;
  late MockIEvenementRepository mockRepository;

  EvenementsBloc buildBloc() => EvenementsBloc(
        mockGetEvents,
        mockGetEventById,
        mockCreateEvent,
        mockUpdateEvent,
        mockDeleteEvent,
        mockRegisterForEvent,
        mockCancelRegistration,
        mockGetEventParticipants,
        mockRepository,
      );

  setUp(() {
    mockGetEvents = MockGetEvents();
    mockGetEventById = MockGetEventById();
    mockCreateEvent = MockCreateEvent();
    mockUpdateEvent = MockUpdateEvent();
    mockDeleteEvent = MockDeleteEvent();
    mockRegisterForEvent = MockRegisterForEvent();
    mockCancelRegistration = MockCancelRegistration();
    mockGetEventParticipants = MockGetEventParticipants();
    mockRepository = MockIEvenementRepository();
  });

  // ---- initial state -------------------------------------------------------

  test('initial state is EvenementsInitial', () {
    final bloc = buildBloc();
    expect(bloc.state, isA<EvenementsInitial>());
    bloc.close();
  });

  // ---- LoadEvenements ------------------------------------------------------

  group('LoadEvenements', () {
    final evenement = _makeEvenement();
    final searchResult = _makeSearchResult([evenement]);

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Loaded] on success',
      build: () {
        when(mockGetEvents(
                page: anyNamed('page'),
                size: anyNamed('size'),
                recherche: anyNamed('recherche')))
            .thenAnswer((_) async => searchResult);
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenements()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsLoaded>()
            .having((s) => s.evenements.length, 'count', 1)
            .having((s) => s.total, 'total', 1),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Refreshing, Loaded] when refresh=true and state is EvenementsLoaded',
      build: () {
        when(mockGetEvents(
                page: anyNamed('page'),
                size: anyNamed('size'),
                recherche: anyNamed('recherche')))
            .thenAnswer((_) async => _makeSearchResult([]));
        return buildBloc();
      },
      seed: () => EvenementsLoaded(
        evenements: [evenement],
        total: 1,
        totalPages: 1,
      ),
      act: (b) => b.add(const LoadEvenements(refresh: true)),
      expect: () => [
        isA<EvenementsRefreshing>()
            .having((s) => s.currentEvenements.length, 'current', 1),
        isA<EvenementsLoaded>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on generic exception',
      build: () {
        when(mockGetEvents(
                page: anyNamed('page'),
                size: anyNamed('size'),
                recherche: anyNamed('recherche')))
            .thenThrow(Exception('network'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenements()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, NetworkError] on DioException (non-auth)',
      build: () {
        when(mockGetEvents(
                page: anyNamed('page'),
                size: anyNamed('size'),
                recherche: anyNamed('recherche')))
            .thenThrow(_makeDioException(500));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenements()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsNetworkError>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementsError] on 401 DioException',
      build: () {
        when(mockGetEvents(
                page: anyNamed('page'),
                size: anyNamed('size'),
                recherche: anyNamed('recherche')))
            .thenThrow(_makeDioException(401));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenements()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'uses recherche parameter',
      build: () {
        when(mockGetEvents(
                page: anyNamed('page'),
                size: anyNamed('size'),
                recherche: 'gala'))
            .thenAnswer((_) async => _makeSearchResult([evenement]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenements(recherche: 'gala')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsLoaded>(),
      ],
      verify: (_) => verify(mockGetEvents(
          page: anyNamed('page'),
          size: anyNamed('size'),
          recherche: 'gala')),
    );
  });

  // ---- LoadEvenementById ---------------------------------------------------

  group('LoadEvenementById', () {
    final evenement = _makeEvenement();

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementDetailLoaded] when found',
      build: () {
        when(mockGetEventById.call(any)).thenAnswer((_) async => evenement);
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementById('1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementDetailLoaded>()
            .having((s) => s.evenement.id, 'id', 1),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementsError(404)] when not found (null)',
      build: () {
        when(mockGetEventById.call(any)).thenAnswer((_) async => null);
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementById('missing')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>()
            .having((s) => s.code, 'code', '404'),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on exception',
      build: () {
        when(mockGetEventById.call(any)).thenThrow(Exception('server error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementById('1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });

  // ---- CreateEvenement -----------------------------------------------------

  group('CreateEvenement', () {
    final evenement = _makeEvenement(id: 99);

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementCreated] on success',
      build: () {
        when(mockCreateEvent.call(any)).thenAnswer((_) async => evenement);
        return buildBloc();
      },
      act: (b) => b.add(CreateEvenement(evenement)),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementCreated>()
            .having((s) => s.evenement.id, 'id', 99),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementsValidationError] on 400 DioException',
      build: () {
        final dioEx = DioException(
          requestOptions: RequestOptions(path: '/test'),
          response: Response(
            requestOptions: RequestOptions(path: '/test'),
            statusCode: 400,
            data: {
              'errors': {'titre': 'obligatoire'}
            },
          ),
          type: DioExceptionType.badResponse,
        );
        when(mockCreateEvent.call(any)).thenThrow(dioEx);
        return buildBloc();
      },
      act: (b) => b.add(CreateEvenement(evenement)),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsValidationError>()
            .having((s) => s.code, 'code', '400'),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on generic exception',
      build: () {
        when(mockCreateEvent.call(any)).thenThrow(Exception('creation failed'));
        return buildBloc();
      },
      act: (b) => b.add(CreateEvenement(evenement)),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });

  // ---- UpdateEvenement -----------------------------------------------------

  group('UpdateEvenement', () {
    final evenement = _makeEvenement();

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementUpdated] on success',
      build: () {
        when(mockUpdateEvent.call(any, any)).thenAnswer((_) async => evenement);
        return buildBloc();
      },
      act: (b) => b.add(UpdateEvenement('1', evenement)),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementUpdated>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, ValidationError] on 400',
      build: () {
        final dioEx = DioException(
          requestOptions: RequestOptions(path: '/test'),
          response: Response(
            requestOptions: RequestOptions(path: '/test'),
            statusCode: 400,
            data: {'errors': {}},
          ),
          type: DioExceptionType.badResponse,
        );
        when(mockUpdateEvent.call(any, any)).thenThrow(dioEx);
        return buildBloc();
      },
      act: (b) => b.add(UpdateEvenement('1', evenement)),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsValidationError>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on generic exception',
      build: () {
        when(mockUpdateEvent.call(any, any))
            .thenThrow(Exception('update failed'));
        return buildBloc();
      },
      act: (b) => b.add(UpdateEvenement('1', evenement)),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });

  // ---- DeleteEvenement -----------------------------------------------------

  group('DeleteEvenement', () {
    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementDeleted] on success',
      build: () {
        when(mockDeleteEvent.call(any)).thenAnswer((_) async => null);
        return buildBloc();
      },
      act: (b) => b.add(const DeleteEvenement('1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementDeleted>()
            .having((s) => s.id, 'id', '1'),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockDeleteEvent.call(any)).thenThrow(Exception('delete failed'));
        return buildBloc();
      },
      act: (b) => b.add(const DeleteEvenement('1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });

  // ---- LoadEvenementsAVenir ------------------------------------------------

  group('LoadEvenementsAVenir', () {
    final result = _makeSearchResult([_makeEvenement()]);

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Loaded] on success',
      build: () {
        when(mockRepository.getEvenementsAVenir(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => result);
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementsAVenir()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsLoaded>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, NetworkError] on DioException',
      build: () {
        when(mockRepository.getEvenementsAVenir(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenThrow(_makeDioException(503));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementsAVenir()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsNetworkError>(),
      ],
    );
  });

  // ---- LoadEvenementsEnCours -----------------------------------------------

  group('LoadEvenementsEnCours', () {
    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Loaded] on success',
      build: () {
        when(mockRepository.getEvenementsEnCours(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => _makeSearchResult([_makeEvenement()]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementsEnCours()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsLoaded>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on generic exception',
      build: () {
        when(mockRepository.getEvenementsEnCours(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenThrow(Exception('server error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementsEnCours()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });

  // ---- LoadEvenementsPasses ------------------------------------------------

  group('LoadEvenementsPasses', () {
    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Loaded] on success',
      build: () {
        when(mockRepository.getEvenementsPasses(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenAnswer((_) async => _makeSearchResult([_makeEvenement()]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementsPasses()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsLoaded>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on generic exception',
      build: () {
        when(mockRepository.getEvenementsPasses(
                page: anyNamed('page'), size: anyNamed('size')))
            .thenThrow(Exception('server error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementsPasses()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });

  // ---- InscrireEvenement ---------------------------------------------------

  group('InscrireEvenement', () {
    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementInscrit] on success',
      build: () {
        when(mockRegisterForEvent.call(any)).thenAnswer((_) async => null);
        return buildBloc();
      },
      act: (b) => b.add(const InscrireEvenement('evt-1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementInscrit>()
            .having((s) => s.evenementId, 'evenementId', 'evt-1'),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockRegisterForEvent.call(any))
            .thenThrow(Exception('registration failed'));
        return buildBloc();
      },
      act: (b) => b.add(const InscrireEvenement('evt-1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, NetworkError] on non-auth DioException',
      build: () {
        when(mockRegisterForEvent.call(any))
            .thenThrow(_makeDioException(409));
        return buildBloc();
      },
      act: (b) => b.add(const InscrireEvenement('evt-1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsNetworkError>(),
      ],
    );
  });

  // ---- DesinscrireEvenement ------------------------------------------------

  group('DesinscrireEvenement', () {
    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementDesinscrit] on success',
      build: () {
        when(mockCancelRegistration.call(any)).thenAnswer((_) async => null);
        return buildBloc();
      },
      act: (b) => b.add(const DesinscrireEvenement('evt-1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementDesinscrit>()
            .having((s) => s.evenementId, 'evenementId', 'evt-1'),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockCancelRegistration.call(any))
            .thenThrow(Exception('cancel failed'));
        return buildBloc();
      },
      act: (b) => b.add(const DesinscrireEvenement('evt-1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });

  // ---- LoadParticipants ----------------------------------------------------

  group('LoadParticipants', () {
    final participants = [
      {'id': 'm1', 'nom': 'Dupont', 'statut': 'CONFIRME'},
      {'id': 'm2', 'nom': 'Martin', 'statut': 'EN_ATTENTE'},
    ];

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, ParticipantsLoaded] on success',
      build: () {
        when(mockGetEventParticipants.call(any))
            .thenAnswer((_) async => participants);
        return buildBloc();
      },
      act: (b) => b.add(const LoadParticipants('evt-1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<ParticipantsLoaded>()
            .having((s) => s.evenementId, 'evenementId', 'evt-1')
            .having((s) => s.participants.length, 'count', 2),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on failure',
      build: () {
        when(mockGetEventParticipants.call(any))
            .thenThrow(Exception('participants error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadParticipants('evt-1')),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });

  // ---- LoadEvenementsStats -------------------------------------------------

  group('LoadEvenementsStats', () {
    final stats = {'total': 20, 'aVenir': 5, 'enCours': 3};

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, EvenementsStatsLoaded] on success',
      build: () {
        when(mockRepository.getEvenementsStats())
            .thenAnswer((_) async => stats);
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementsStats()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsStatsLoaded>()
            .having((s) => s.stats['total'], 'total', 20),
      ],
    );

    blocTest<EvenementsBloc, EvenementsState>(
      'emits [Loading, Error] on exception',
      build: () {
        when(mockRepository.getEvenementsStats())
            .thenThrow(Exception('stats error'));
        return buildBloc();
      },
      act: (b) => b.add(const LoadEvenementsStats()),
      expect: () => [
        isA<EvenementsLoading>(),
        isA<EvenementsError>(),
      ],
    );
  });
}
