import 'package:afterwork/data/datasources/event_remote_data_source.dart';
import 'package:afterwork/data/models/event_model.dart';
import 'package:afterwork/presentation/state_management/event_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

// Mock classes
class MockEventRemoteDataSource extends Mock
    implements EventRemoteDataSource {}

void main() {
  late EventBloc eventBloc;
  late MockEventRemoteDataSource mockRemoteDataSource;

  setUpAll(() {
    // Enregistrer fallback value pour EventModel
    registerFallbackValue(
      EventModel(
        id: 'fallback',
        title: 'Fallback',
        description: 'Fallback',
        startDate: '2026-01-01T00:00:00Z',
        location: 'Fallback',
        category: 'Fallback',
        link: 'Fallback',
        creatorEmail: 'fallback@example.com',
        creatorFirstName: 'Fallback',
        creatorLastName: 'Fallback',
        profileImageUrl: 'fallback.jpg',
        participants: const [],
        status: 'ouvert',
        reactionsCount: 0,
        commentsCount: 0,
        sharesCount: 0,
      ),
    );
  });

  setUp(() {
    mockRemoteDataSource = MockEventRemoteDataSource();
    eventBloc = EventBloc(remoteDataSource: mockRemoteDataSource);
  });

  tearDown(() {
    eventBloc.close();
  });

  final tEventModels = [
    EventModel(
      id: '1',
      title: 'Event 1',
      description: 'Description 1',
      startDate: '2026-01-15T19:00:00Z',
      location: 'Location 1',
      category: 'Category 1',
      link: 'https://link1.com',
      creatorEmail: 'creator1@example.com',
      creatorFirstName: 'John',
      creatorLastName: 'Doe',
      profileImageUrl: 'profile1.jpg',
      participants: const [],
      status: 'ouvert',
      reactionsCount: 0,
      commentsCount: 0,
      sharesCount: 0,
    ),
    EventModel(
      id: '2',
      title: 'Event 2',
      description: 'Description 2',
      startDate: '2026-01-16T19:00:00Z',
      location: 'Location 2',
      category: 'Category 2',
      link: 'https://link2.com',
      creatorEmail: 'creator2@example.com',
      creatorFirstName: 'Jane',
      creatorLastName: 'Smith',
      profileImageUrl: 'profile2.jpg',
      participants: const [],
      status: 'ouvert',
      reactionsCount: 0,
      commentsCount: 0,
      sharesCount: 0,
    ),
  ];

  group('EventBloc', () {
    test('initial state should be EventInitial', () {
      // Assert
      expect(eventBloc.state, isA<EventInitial>());
    });

    group('LoadEvents', () {
      blocTest<EventBloc, EventState>(
        'should emit [EventLoading, EventLoaded] when data is gotten successfully',
        build: () {
          when(
            () => mockRemoteDataSource
                .getEventsCreatedByUserAndFriends(any()),
          ).thenAnswer((_) async => tEventModels);
          return eventBloc;
        },
        act: (bloc) => bloc.add(const LoadEvents('user123')),
        expect: () => [
          isA<EventLoading>(),
          isA<EventLoaded>().having((s) => s.events, 'events', tEventModels),
        ],
        verify: (_) {
          verify(
            () => mockRemoteDataSource
                .getEventsCreatedByUserAndFriends('user123'),
          ).called(1);
        },
      );

      blocTest<EventBloc, EventState>(
        'should emit [EventLoading, EventError] when getting data fails',
        build: () {
          when(
            () => mockRemoteDataSource
                .getEventsCreatedByUserAndFriends(any()),
          ).thenThrow(Exception('Failed to load events'));
          return eventBloc;
        },
        act: (bloc) => bloc.add(const LoadEvents('user123')),
        expect: () => [
          isA<EventLoading>(),
          isA<EventError>()
              .having((s) => s.message, 'message', 'Erreur lors du chargement des événements.'),
        ],
      );

      blocTest<EventBloc, EventState>(
        'should emit empty list when no events found',
        build: () {
          when(
            () => mockRemoteDataSource
                .getEventsCreatedByUserAndFriends(any()),
          ).thenAnswer((_) async => []);
          return eventBloc;
        },
        act: (bloc) => bloc.add(const LoadEvents('user123')),
        expect: () => [
          isA<EventLoading>(),
          isA<EventLoaded>().having((s) => s.events, 'events', isEmpty),
        ],
      );
    });

    group('AddEvent', () {
      final tNewEvent = EventModel(
        id: '3',
        title: 'New Event',
        description: 'New Description',
        startDate: '2026-01-17T19:00:00Z',
        location: 'New Location',
        category: 'New Category',
        link: 'https://newlink.com',
        creatorEmail: 'new@example.com',
        creatorFirstName: 'New',
        creatorLastName: 'Creator',
        profileImageUrl: 'newprofile.jpg',
        participants: const [],
        status: 'ouvert',
        reactionsCount: 0,
        commentsCount: 0,
        sharesCount: 0,
      );

      blocTest<EventBloc, EventState>(
        'should emit [EventLoading, EventLoaded] when event is added successfully',
        build: () {
          when(() => mockRemoteDataSource.createEvent(any()))
              .thenAnswer((_) async => tNewEvent);
          when(() => mockRemoteDataSource.getAllEvents())
              .thenAnswer((_) async => [...tEventModels, tNewEvent]);
          return eventBloc;
        },
        act: (bloc) => bloc.add(AddEvent(tNewEvent)),
        expect: () => [
          isA<EventLoading>(),
          isA<EventLoaded>()
              .having((s) => s.events.length, 'events.length', 3)
              .having((s) => s.events.last.id, 'last event id', '3'),
        ],
      );

      blocTest<EventBloc, EventState>(
        'should emit [EventLoading, EventError] when adding event fails',
        build: () {
          when(() => mockRemoteDataSource.createEvent(any()))
              .thenThrow(Exception('Failed to create event'));
          return eventBloc;
        },
        act: (bloc) => bloc.add(AddEvent(tNewEvent)),
        expect: () => [
          isA<EventLoading>(),
          isA<EventError>()
              .having((s) => s.message, 'message', "Erreur lors de l'ajout de l'événement."),
        ],
      );
    });

    group('CloseEvent', () {
      blocTest<EventBloc, EventState>(
        'should emit [EventLoading, EventLoaded] with updated event status',
        build: () {
          when(() => mockRemoteDataSource.closeEvent(any()))
              .thenAnswer((_) async => {});
          return eventBloc;
        },
        seed: () => EventLoaded(tEventModels),
        act: (bloc) => bloc.add(const CloseEvent('1')),
        wait: const Duration(milliseconds: 100),
        expect: () => [
          isA<EventLoading>(),
          predicate<EventState>((state) {
            if (state is EventLoaded) {
              try {
                final event = state.events.firstWhere((e) => e.id == '1');
                return event.status == 'fermé';
              } catch (_) {
                return false;
              }
            }
            return false;
          }),
        ],
      );

      blocTest<EventBloc, EventState>(
        'should emit [EventLoading, EventError] when closing event fails',
        build: () {
          when(() => mockRemoteDataSource.closeEvent(any()))
              .thenThrow(Exception('Failed to close event'));
          return eventBloc;
        },
        seed: () => EventLoaded(tEventModels),
        act: (bloc) => bloc.add(const CloseEvent('1')),
        expect: () => [
          isA<EventLoading>(),
          isA<EventError>()
              .having((s) => s.message, 'message', "Erreur lors de la fermeture de l'événement."),
        ],
      );

      blocTest<EventBloc, EventState>(
        'should not update state when event is not in loaded list',
        build: () {
          when(() => mockRemoteDataSource.closeEvent(any()))
              .thenAnswer((_) async => {});
          return eventBloc;
        },
        seed: () => EventLoaded(tEventModels),
        act: (bloc) => bloc.add(const CloseEvent('non-existent-id')),
        expect: () => [
          isA<EventLoading>(),
          isA<EventLoaded>(), // Restaure l'état précédent
        ],
      );
    });

    group('ReopenEvent', () {
      final closedEvents = tEventModels.map((e) {
        final event = EventModel(
          id: e.id,
          title: e.title,
          description: e.description,
          startDate: e.startDate,
          location: e.location,
          category: e.category,
          link: e.link,
          creatorEmail: e.creatorEmail,
          creatorFirstName: e.creatorFirstName,
          creatorLastName: e.creatorLastName,
          profileImageUrl: e.profileImageUrl,
          participants: e.participants,
          status: 'fermé',
          reactionsCount: e.reactionsCount,
          commentsCount: e.commentsCount,
          sharesCount: e.sharesCount,
        );
        return event;
      }).toList();

      blocTest<EventBloc, EventState>(
        'should emit [EventLoading, EventLoaded] with reopened event',
        build: () {
          when(() => mockRemoteDataSource.reopenEvent(any()))
              .thenAnswer((_) async => {});
          return eventBloc;
        },
        seed: () => EventLoaded(closedEvents),
        act: (bloc) => bloc.add(const ReopenEvent('1')),
        wait: const Duration(milliseconds: 100),
        expect: () => [
          isA<EventLoading>(),
          predicate<EventState>((state) {
            if (state is EventLoaded) {
              try {
                final event = state.events.firstWhere((e) => e.id == '1');
                return event.status == 'ouvert';
              } catch (_) {
                return false;
              }
            }
            return false;
          }),
        ],
      );

      blocTest<EventBloc, EventState>(
        'should emit [EventLoading, EventError] when reopening event fails',
        build: () {
          when(() => mockRemoteDataSource.reopenEvent(any()))
              .thenThrow(Exception('Failed to reopen event'));
          return eventBloc;
        },
        seed: () => EventLoaded(closedEvents),
        act: (bloc) => bloc.add(const ReopenEvent('1')),
        expect: () => [
          isA<EventLoading>(),
          isA<EventError>()
              .having((s) => s.message, 'message', "Erreur lors de la réouverture de l'événement."),
        ],
      );

      blocTest<EventBloc, EventState>(
        'should not update state when reopening non-existent event',
        build: () {
          when(() => mockRemoteDataSource.reopenEvent(any()))
              .thenAnswer((_) async => {});
          return eventBloc;
        },
        seed: () => EventLoaded(closedEvents),
        act: (bloc) => bloc.add(const ReopenEvent('non-existent-id')),
        expect: () => [
          isA<EventLoading>(),
          isA<EventLoaded>(), // Restaure l'état précédent
        ],
      );
    });

    group('RemoveEvent', () {
      blocTest<EventBloc, EventState>(
        'should remove event from list locally',
        build: () => eventBloc,
        seed: () => EventLoaded(tEventModels),
        act: (bloc) => bloc.add(const RemoveEvent('1')),
        skip: 0,
        expect: () => [
          isA<EventLoaded>().having(
            (s) => s.events.length,
            'events.length',
            1,
          ).having(
            (s) => s.events.first.id,
            'first event id',
            '2',
          ),
        ],
      );

      blocTest<EventBloc, EventState>(
        'should not emit anything when state is not EventLoaded',
        build: () => eventBloc,
        seed: () => const EventInitial(),
        act: (bloc) => bloc.add(const RemoveEvent('1')),
        expect: () => [],
      );

      test('should handle removing non-existent event gracefully', () async {
        // Arrange
        final bloc = eventBloc;
        final initialEvents = tEventModels;
        
        // Act
        bloc.emit(EventLoaded(initialEvents));
        bloc.add(const RemoveEvent('non-existent-id'));
        
        // Wait for the event to be processed
        await Future.delayed(const Duration(milliseconds: 100));
        
        // Assert
        final currentState = bloc.state;
        expect(currentState, isA<EventLoaded>());
        if (currentState is EventLoaded) {
          expect(currentState.events.length, 2); // La liste reste inchangée
        }
      });
    });
  });
}

