import 'dart:async';

import 'package:bloc_test/bloc_test.dart';
import 'package:dartz/dartz.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/error/failures.dart';
import 'package:unionflow_mobile_apps/core/websocket/websocket_service.dart';
import 'package:unionflow_mobile_apps/features/dashboard/domain/entities/dashboard_entity.dart';
import 'package:unionflow_mobile_apps/features/dashboard/domain/usecases/get_dashboard_data.dart';
import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/dashboard_bloc.dart';

@GenerateMocks([
  GetDashboardData,
  GetDashboardStats,
  GetRecentActivities,
  GetUpcomingEvents,
  WebSocketService,
])
import 'dashboard_bloc_test.mocks.dart';

// ---------------------------------------------------------------------------
// Test fixtures
// ---------------------------------------------------------------------------

DashboardStatsEntity _buildStats() => DashboardStatsEntity(
      totalMembers: 100,
      activeMembers: 80,
      totalEvents: 10,
      upcomingEvents: 3,
      totalContributions: 50,
      totalContributionAmount: 500000,
      pendingRequests: 5,
      completedProjects: 2,
      monthlyGrowth: 0.05,
      engagementRate: 0.8,
      lastUpdated: DateTime(2026, 4, 20),
    );

DashboardEntity _buildDashboardEntity({
  String orgId = 'org-1',
  String userId = 'user-1',
}) =>
    DashboardEntity(
      stats: _buildStats(),
      recentActivities: const [],
      upcomingEvents: const [],
      userPreferences: const {},
      organizationId: orgId,
      userId: userId,
    );

// ---------------------------------------------------------------------------
// Helpers to keep WebSocketService mock compiling without excessive stub noise
// ---------------------------------------------------------------------------

void _stubWebSocket(MockWebSocketService ws) {
  // Provide broadcast streams so the bloc can subscribe without NPE
  final eventController = StreamController<WebSocketEvent>.broadcast();
  final statusController = StreamController<bool>.broadcast();

  when(ws.eventStream).thenAnswer((_) => eventController.stream);
  when(ws.connectionStatusStream).thenAnswer((_) => statusController.stream);
  when(ws.connect()).thenReturn(null);
  when(ws.disconnect()).thenReturn(null);
  when(ws.isConnected).thenReturn(false);
}

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

void main() {
  // AppConfig uses late final statics — initialize once before any tests run
  setUpAll(() {
    try {
      AppConfig.initialize();
    } catch (_) {
      // Already initialized by a previous test suite in the same process
    }
  });

  late MockGetDashboardData mockGetDashboardData;
  late MockGetDashboardStats mockGetDashboardStats;
  late MockGetRecentActivities mockGetRecentActivities;
  late MockGetUpcomingEvents mockGetUpcomingEvents;
  late MockWebSocketService mockWebSocketService;

  setUp(() {
    mockGetDashboardData = MockGetDashboardData();
    mockGetDashboardStats = MockGetDashboardStats();
    mockGetRecentActivities = MockGetRecentActivities();
    mockGetUpcomingEvents = MockGetUpcomingEvents();
    mockWebSocketService = MockWebSocketService();
    _stubWebSocket(mockWebSocketService);
  });

  DashboardBloc buildBloc() => DashboardBloc(
        getDashboardData: mockGetDashboardData,
        getDashboardStats: mockGetDashboardStats,
        getRecentActivities: mockGetRecentActivities,
        getUpcomingEvents: mockGetUpcomingEvents,
        webSocketService: mockWebSocketService,
      );

  // blocs are closed inside blocTest automatically — no explicit tearDown needed

  // ─── initial state ───────────────────────────────────────────────────────

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

  // ─── LoadDashboardData ───────────────────────────────────────────────────

  group('LoadDashboardData', () {
    final entity = _buildDashboardEntity();

    blocTest<DashboardBloc, DashboardState>(
      'emits [DashboardLoading, DashboardLoaded] on success',
      build: () {
        when(mockGetDashboardData(any))
            .thenAnswer((_) async => Right(entity));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [
        isA<DashboardLoading>(),
        isA<DashboardLoaded>(),
      ],
      verify: (_) {
        verify(mockGetDashboardData(
          const GetDashboardDataParams(
            organizationId: 'org-1',
            userId: 'user-1',
          ),
        )).called(1);
      },
    );

    blocTest<DashboardBloc, DashboardState>(
      'emits [DashboardLoading, DashboardLoaded] with correct entity',
      build: () {
        when(mockGetDashboardData(any))
            .thenAnswer((_) async => Right(entity));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [
        isA<DashboardLoading>(),
        DashboardLoaded(entity),
      ],
    );

    blocTest<DashboardBloc, DashboardState>(
      'emits [DashboardLoading, DashboardError] on ServerFailure',
      build: () {
        when(mockGetDashboardData(any))
            .thenAnswer((_) async => const Left(ServerFailure('server error')));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [
        isA<DashboardLoading>(),
        isA<DashboardError>(),
      ],
    );

    blocTest<DashboardBloc, DashboardState>(
      'error message for NetworkFailure is user-friendly',
      build: () {
        when(mockGetDashboardData(any))
            .thenAnswer((_) async => const Left(NetworkFailure('no internet')));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [
        isA<DashboardLoading>(),
        const DashboardError(
          'Pas de connexion internet. Vérifiez votre connexion.',
        ),
      ],
    );

    blocTest<DashboardBloc, DashboardState>(
      'error message for UnauthorizedFailure is user-friendly',
      build: () {
        when(mockGetDashboardData(any)).thenAnswer(
            (_) async => const Left(UnauthorizedFailure('401')));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [
        isA<DashboardLoading>(),
        const DashboardError(
          'Session expirée. Veuillez vous reconnecter.',
        ),
      ],
    );

    blocTest<DashboardBloc, DashboardState>(
      'emits DashboardMemberNotRegistered on NotFoundFailure',
      build: () {
        when(mockGetDashboardData(any)).thenAnswer(
          (_) async => const Left(
            NotFoundFailure('membre non inscrit'),
          ),
        );
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [
        isA<DashboardLoading>(),
        isA<DashboardMemberNotRegistered>(),
      ],
    );

    blocTest<DashboardBloc, DashboardState>(
      'passes useGlobalDashboard flag to use case',
      build: () {
        when(mockGetDashboardData(any))
            .thenAnswer((_) async => Right(entity));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardData(
          organizationId: '',
          userId: 'super-admin',
          useGlobalDashboard: true,
        ),
      ),
      verify: (_) {
        verify(mockGetDashboardData(
          const GetDashboardDataParams(
            organizationId: '',
            userId: 'super-admin',
            useGlobalDashboard: true,
          ),
        )).called(1);
      },
    );
  });

  // ─── RefreshDashboardData ────────────────────────────────────────────────

  group('RefreshDashboardData', () {
    final entity = _buildDashboardEntity();

    blocTest<DashboardBloc, DashboardState>(
      'emits [DashboardLoading, DashboardLoaded] when state is not loaded',
      build: () {
        when(mockGetDashboardData(any))
            .thenAnswer((_) async => Right(entity));
        return buildBloc();
      },
      act: (b) => b.add(
        const RefreshDashboardData(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [
        isA<DashboardLoading>(),
        isA<DashboardLoaded>(),
      ],
    );

    test('emits [DashboardRefreshing, DashboardLoaded] when state is DashboardLoaded', () async {
      when(mockGetDashboardData(any)).thenAnswer((_) async => Right(entity));

      final bloc = buildBloc();
      bloc.add(const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'));
      await bloc.stream.firstWhere((s) => s is DashboardLoaded);

      final states = <DashboardState>[];
      final sub = bloc.stream.listen(states.add);

      bloc.add(const RefreshDashboardData(organizationId: 'org-1', userId: 'user-1'));
      await bloc.stream.firstWhere((s) => s is DashboardLoaded || s is DashboardError);

      await sub.cancel();
      expect(states, containsAll([isA<DashboardRefreshing>(), isA<DashboardLoaded>()]));

      await bloc.close();
    });

    blocTest<DashboardBloc, DashboardState>(
      'emits DashboardError on failure during refresh',
      build: () {
        when(mockGetDashboardData(any))
            .thenAnswer((_) async => const Left(ServerFailure('err')));
        return buildBloc();
      },
      act: (b) => b.add(
        const RefreshDashboardData(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [
        isA<DashboardLoading>(),
        isA<DashboardError>(),
      ],
    );
  });

  // ─── LoadDashboardStats ──────────────────────────────────────────────────

  group('LoadDashboardStats', () {
    final entity = _buildDashboardEntity();
    final stats = _buildStats();

    // BLoC 9.x processes events concurrently. LoadDashboardStats only emits when
    // state is DashboardLoaded. We verify the use case IS called and, when the
    // state is Initial (not loaded), nothing is emitted.
    blocTest<DashboardBloc, DashboardState>(
      'calls getDashboardStats use case with correct params',
      build: () {
        when(mockGetDashboardData(any))
            .thenAnswer((_) async => Right(entity));
        when(mockGetDashboardStats(any))
            .thenAnswer((_) async => Right(stats));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardStats(organizationId: 'org-1', userId: 'user-1'),
      ),
      verify: (_) {
        verify(mockGetDashboardStats(
          const GetDashboardStatsParams(
            organizationId: 'org-1',
            userId: 'user-1',
          ),
        )).called(1);
      },
    );

    blocTest<DashboardBloc, DashboardState>(
      'emits nothing when state is not DashboardLoaded (stats not injected)',
      build: () {
        when(mockGetDashboardStats(any))
            .thenAnswer((_) async => Right(stats));
        return buildBloc();
      },
      // state is DashboardInitial — LoadDashboardStats won't emit
      act: (b) => b.add(
        const LoadDashboardStats(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => <DashboardState>[],
    );

    // Stats failure always emits DashboardError regardless of current state
    blocTest<DashboardBloc, DashboardState>(
      'emits DashboardError on stats failure',
      build: () {
        when(mockGetDashboardStats(any))
            .thenAnswer((_) async => const Left(ServerFailure('stats error')));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadDashboardStats(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => [isA<DashboardError>()],
    );
  });

  // ─── LoadRecentActivities ────────────────────────────────────────────────

  group('LoadRecentActivities', () {
    final entity = _buildDashboardEntity();

    final activity = RecentActivityEntity(
      id: 'act-1',
      type: 'contribution',
      title: 'Cotisation payée',
      description: 'Cotisation mensuelle',
      userName: 'Alice',
      timestamp: DateTime(2026, 4, 19),
    );

    test('updates activities in DashboardLoaded on success', () async {
      when(mockGetDashboardData(any)).thenAnswer((_) async => Right(entity));
      when(mockGetRecentActivities(any)).thenAnswer((_) async => Right([activity]));

      final bloc = buildBloc();
      bloc.add(const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'));
      await bloc.stream.firstWhere((s) => s is DashboardLoaded);

      bloc.add(const LoadRecentActivities(organizationId: 'org-1', userId: 'user-1'));
      final next = await bloc.stream.firstWhere((s) => s is DashboardLoaded || s is DashboardError);

      expect(next, isA<DashboardLoaded>());
      final loaded = next as DashboardLoaded;
      expect(loaded.dashboardData.recentActivities, [activity]);

      await bloc.close();
    });

    blocTest<DashboardBloc, DashboardState>(
      'does nothing when state is not DashboardLoaded',
      build: () {
        when(mockGetRecentActivities(any))
            .thenAnswer((_) async => Right([activity]));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadRecentActivities(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => <DashboardState>[],
    );

    test('emits DashboardError on failure when state is DashboardLoaded', () async {
      when(mockGetDashboardData(any)).thenAnswer((_) async => Right(entity));
      when(mockGetRecentActivities(any))
          .thenAnswer((_) async => const Left(NetworkFailure('net')));

      final bloc = buildBloc();
      bloc.add(const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'));
      await bloc.stream.firstWhere((s) => s is DashboardLoaded);

      bloc.add(const LoadRecentActivities(organizationId: 'org-1', userId: 'user-1'));
      final next = await bloc.stream.firstWhere((s) => s is DashboardError || s is DashboardLoaded);

      expect(next, isA<DashboardError>());
      await bloc.close();
    });

    test('passes custom limit to use case', () async {
      when(mockGetDashboardData(any)).thenAnswer((_) async => Right(entity));
      when(mockGetRecentActivities(any)).thenAnswer((_) async => Right([activity]));

      final bloc = buildBloc();
      bloc.add(const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'));
      await bloc.stream.firstWhere((s) => s is DashboardLoaded);

      bloc.add(const LoadRecentActivities(
        organizationId: 'org-1',
        userId: 'user-1',
        limit: 20,
      ));
      await bloc.stream.firstWhere((s) => s is DashboardLoaded || s is DashboardError);

      verify(mockGetRecentActivities(
        const GetRecentActivitiesParams(
          organizationId: 'org-1',
          userId: 'user-1',
          limit: 20,
        ),
      )).called(1);

      await bloc.close();
    });
  });

  // ─── LoadUpcomingEvents ──────────────────────────────────────────────────

  group('LoadUpcomingEvents', () {
    final entity = _buildDashboardEntity();

    final upcomingEvent = UpcomingEventEntity(
      id: 'evt-1',
      title: 'AG annuelle',
      description: 'Assemblée générale',
      startDate: DateTime(2026, 5, 1),
      location: 'Dakar',
      maxParticipants: 100,
      currentParticipants: 40,
      status: 'planifié',
      tags: const [],
    );

    test('updates events in DashboardLoaded on success', () async {
      when(mockGetDashboardData(any)).thenAnswer((_) async => Right(entity));
      when(mockGetUpcomingEvents(any)).thenAnswer((_) async => Right([upcomingEvent]));

      final bloc = buildBloc();
      bloc.add(const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'));
      await bloc.stream.firstWhere((s) => s is DashboardLoaded);

      bloc.add(const LoadUpcomingEvents(organizationId: 'org-1', userId: 'user-1'));
      final next = await bloc.stream.firstWhere((s) => s is DashboardLoaded || s is DashboardError);

      expect(next, isA<DashboardLoaded>());
      final loaded = next as DashboardLoaded;
      expect(loaded.dashboardData.upcomingEvents, [upcomingEvent]);

      await bloc.close();
    });

    blocTest<DashboardBloc, DashboardState>(
      'does nothing when state is not DashboardLoaded',
      build: () {
        when(mockGetUpcomingEvents(any))
            .thenAnswer((_) async => Right([upcomingEvent]));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadUpcomingEvents(organizationId: 'org-1', userId: 'user-1'),
      ),
      expect: () => <DashboardState>[],
    );

    test('emits DashboardError on failure when state is DashboardLoaded', () async {
      when(mockGetDashboardData(any)).thenAnswer((_) async => Right(entity));
      when(mockGetUpcomingEvents(any))
          .thenAnswer((_) async => const Left(ServerFailure('err')));

      final bloc = buildBloc();
      bloc.add(const LoadDashboardData(organizationId: 'org-1', userId: 'user-1'));
      await bloc.stream.firstWhere((s) => s is DashboardLoaded);

      bloc.add(const LoadUpcomingEvents(organizationId: 'org-1', userId: 'user-1'));
      final next = await bloc.stream.firstWhere((s) => s is DashboardError || s is DashboardLoaded);

      expect(next, isA<DashboardError>());
      await bloc.close();
    });
  });

  // ─── RefreshDashboardFromWebSocket ───────────────────────────────────────

  group('RefreshDashboardFromWebSocket', () {
    final stats = _buildStats();

    // BLoC 9 concurrent: RefreshDashboardFromWebSocket calls getDashboardStats
    // only when state is DashboardLoaded. We verify the call is made.
    blocTest<DashboardBloc, DashboardState>(
      'calls getDashboardStats when triggered from WebSocket (state not loaded — no call)',
      build: () {
        when(mockGetDashboardStats(any))
            .thenAnswer((_) async => Right(stats));
        return buildBloc();
      },
      act: (b) => b.add(
        const RefreshDashboardFromWebSocket({'type': 'DASHBOARD_STATS_UPDATED'}),
      ),
      // state is DashboardInitial — getDashboardStats NOT called
      verify: (_) => verifyZeroInteractions(mockGetDashboardStats),
    );

    // WebSocket refresh with failure swallows error silently
    blocTest<DashboardBloc, DashboardState>(
      'does not emit error when getDashboardStats fails during WebSocket refresh',
      build: () {
        when(mockGetDashboardStats(any))
            .thenAnswer((_) async => const Left(NetworkFailure('net')));
        return buildBloc();
      },
      // state is DashboardInitial — nothing happens (getDashboardStats not called)
      act: (b) => b.add(
        const RefreshDashboardFromWebSocket({'type': 'CONTRIBUTION_PAID'}),
      ),
      expect: () => <DashboardState>[],
    );

    blocTest<DashboardBloc, DashboardState>(
      'does nothing when state is not DashboardLoaded',
      build: () {
        return buildBloc();
      },
      act: (b) => b.add(
        const RefreshDashboardFromWebSocket({'type': 'DASHBOARD_STATS_UPDATED'}),
      ),
      expect: () => <DashboardState>[],
      verify: (_) => verifyZeroInteractions(mockGetDashboardStats),
    );
  });

  // ─── WebSocketConnectionChanged ──────────────────────────────────────────

  group('WebSocketConnectionChanged', () {
    blocTest<DashboardBloc, DashboardState>(
      'emits nothing (no state change) on connected = true',
      build: buildBloc,
      act: (b) => b.add(const WebSocketConnectionChanged(true)),
      expect: () => <DashboardState>[],
    );

    blocTest<DashboardBloc, DashboardState>(
      'emits nothing (no state change) on connected = false',
      build: buildBloc,
      act: (b) => b.add(const WebSocketConnectionChanged(false)),
      expect: () => <DashboardState>[],
    );
  });

  // ─── close() ─────────────────────────────────────────────────────────────

  test('close() disconnects WebSocket', () async {
    final bloc = buildBloc();
    await bloc.close();
    verify(mockWebSocketService.disconnect()).called(1);
  });
}
