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/dashboard/data/repositories/finance_repository.dart';
import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_bloc.dart';
import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_event.dart';
import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_state.dart';

@GenerateMocks([FinanceRepository])
import 'finance_bloc_test.mocks.dart';

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

const _summary = FinanceSummary(
  totalContributionsPaid: 150000,
  totalContributionsPending: 25000,
  epargneBalance: 300000,
);

final _transactions = [
  const FinanceTransaction(
    id: 'tx-1',
    title: 'Cotisation Janvier',
    date: '01/01/2026',
    amount: 5000,
    status: 'En attente',
  ),
  const FinanceTransaction(
    id: 'tx-2',
    title: 'Cotisation Février',
    date: '01/02/2026',
    amount: 5000,
    status: 'Payé',
  ),
];

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

void main() {
  late MockFinanceRepository mockRepository;

  setUp(() {
    mockRepository = MockFinanceRepository();
  });

  FinanceBloc buildBloc() => FinanceBloc(mockRepository);

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

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

  // ─── LoadFinanceRequested ────────────────────────────────────────────────

  group('LoadFinanceRequested', () {
    blocTest<FinanceBloc, FinanceState>(
      'emits [FinanceLoading, FinanceLoaded] on success',
      build: () {
        when(mockRepository.getFinancialSummary())
            .thenAnswer((_) async => _summary);
        when(mockRepository.getTransactions())
            .thenAnswer((_) async => _transactions);
        return buildBloc();
      },
      act: (b) => b.add(LoadFinanceRequested()),
      expect: () => [
        isA<FinanceLoading>(),
        isA<FinanceLoaded>(),
      ],
      verify: (_) {
        verify(mockRepository.getFinancialSummary()).called(1);
        verify(mockRepository.getTransactions()).called(1);
      },
    );

    blocTest<FinanceBloc, FinanceState>(
      'FinanceLoaded contains correct summary and transactions',
      build: () {
        when(mockRepository.getFinancialSummary())
            .thenAnswer((_) async => _summary);
        when(mockRepository.getTransactions())
            .thenAnswer((_) async => _transactions);
        return buildBloc();
      },
      act: (b) => b.add(LoadFinanceRequested()),
      expect: () => [
        isA<FinanceLoading>(),
        FinanceLoaded(summary: _summary, transactions: _transactions),
      ],
    );

    blocTest<FinanceBloc, FinanceState>(
      'emits [FinanceLoading, FinanceLoaded] with empty transactions list',
      build: () {
        when(mockRepository.getFinancialSummary())
            .thenAnswer((_) async => _summary);
        when(mockRepository.getTransactions())
            .thenAnswer((_) async => []);
        return buildBloc();
      },
      act: (b) => b.add(LoadFinanceRequested()),
      expect: () => [
        isA<FinanceLoading>(),
        const FinanceLoaded(summary: _summary, transactions: []),
      ],
    );

    blocTest<FinanceBloc, FinanceState>(
      'emits [FinanceLoading, FinanceError] when getFinancialSummary throws',
      build: () {
        when(mockRepository.getFinancialSummary())
            .thenThrow(Exception('network error'));
        when(mockRepository.getTransactions())
            .thenAnswer((_) async => []);
        return buildBloc();
      },
      act: (b) => b.add(LoadFinanceRequested()),
      expect: () => [
        isA<FinanceLoading>(),
        isA<FinanceError>(),
      ],
    );

    blocTest<FinanceBloc, FinanceState>(
      'emits [FinanceLoading, FinanceError] when getTransactions throws',
      build: () {
        when(mockRepository.getFinancialSummary())
            .thenAnswer((_) async => _summary);
        when(mockRepository.getTransactions())
            .thenThrow(Exception('transactions error'));
        return buildBloc();
      },
      act: (b) => b.add(LoadFinanceRequested()),
      expect: () => [
        isA<FinanceLoading>(),
        isA<FinanceError>(),
      ],
    );

    blocTest<FinanceBloc, FinanceState>(
      'FinanceError message contains error information',
      build: () {
        when(mockRepository.getFinancialSummary())
            .thenThrow(Exception('server down'));
        when(mockRepository.getTransactions())
            .thenAnswer((_) async => []);
        return buildBloc();
      },
      act: (b) => b.add(LoadFinanceRequested()),
      expect: () => [
        isA<FinanceLoading>(),
        predicate<FinanceState>(
          (s) => s is FinanceError && s.message.contains('Erreur chargement'),
          'FinanceError with expected prefix',
        ),
      ],
    );

    blocTest<FinanceBloc, FinanceState>(
      'does NOT emit error when DioException type is cancel',
      build: () {
        final dioException = DioException(
          requestOptions: RequestOptions(path: '/test'),
          type: DioExceptionType.cancel,
        );
        when(mockRepository.getFinancialSummary())
            .thenThrow(dioException);
        when(mockRepository.getTransactions())
            .thenAnswer((_) async => []);
        return buildBloc();
      },
      act: (b) => b.add(LoadFinanceRequested()),
      // Only FinanceLoading emitted; the cancel exception is swallowed
      expect: () => [isA<FinanceLoading>()],
    );

    blocTest<FinanceBloc, FinanceState>(
      'handles multiple consecutive LoadFinanceRequested events',
      build: () {
        when(mockRepository.getFinancialSummary())
            .thenAnswer((_) async => _summary);
        when(mockRepository.getTransactions())
            .thenAnswer((_) async => _transactions);
        return buildBloc();
      },
      act: (b) async {
        b.add(LoadFinanceRequested());
        // Wait for the first load to complete before firing the second
        await b.stream.firstWhere((s) => s is FinanceLoaded);
        b.add(LoadFinanceRequested());
      },
      expect: () => [
        isA<FinanceLoading>(),
        isA<FinanceLoaded>(),
        isA<FinanceLoading>(),
        isA<FinanceLoaded>(),
      ],
    );
  });

  // ─── FinancePaymentInitiated ─────────────────────────────────────────────

  group('FinancePaymentInitiated', () {
    blocTest<FinanceBloc, FinanceState>(
      'does not change state when current state is FinanceInitial',
      build: buildBloc,
      act: (b) => b.add(const FinancePaymentInitiated('contrib-1')),
      expect: () => <FinanceState>[],
    );

    blocTest<FinanceBloc, FinanceState>(
      'does not change state when current state is FinanceLoaded (no-op for now)',
      build: () {
        // No repository calls needed — the handler is a no-op when loaded
        return buildBloc();
      },
      seed: () =>
          FinanceLoaded(summary: _summary, transactions: _transactions),
      act: (b) =>
          b.add(const FinancePaymentInitiated('contrib-42')),
      // The bloc intentionally keeps FinanceLoaded unchanged
      expect: () => <FinanceState>[],
    );

    blocTest<FinanceBloc, FinanceState>(
      'does not call any repository method',
      build: buildBloc,
      act: (b) => b.add(const FinancePaymentInitiated('contrib-1')),
      verify: (_) {
        verifyZeroInteractions(mockRepository);
      },
    );
  });

  // ─── State equality ──────────────────────────────────────────────────────

  group('State equality', () {
    test('FinanceSummary equality works correctly', () {
      const s1 = FinanceSummary(
        totalContributionsPaid: 100,
        totalContributionsPending: 50,
        epargneBalance: 200,
      );
      const s2 = FinanceSummary(
        totalContributionsPaid: 100,
        totalContributionsPending: 50,
        epargneBalance: 200,
      );
      expect(s1, equals(s2));
    });

    test('FinanceTransaction equality works correctly', () {
      const tx1 = FinanceTransaction(
        id: 'tx-1',
        title: 'Title',
        date: '01/01/2026',
        amount: 5000,
        status: 'Payé',
      );
      const tx2 = FinanceTransaction(
        id: 'tx-1',
        title: 'Title',
        date: '01/01/2026',
        amount: 5000,
        status: 'Payé',
      );
      expect(tx1, equals(tx2));
    });

    test('FinanceLoaded equality works correctly', () {
      final l1 =
          FinanceLoaded(summary: _summary, transactions: _transactions);
      final l2 =
          FinanceLoaded(summary: _summary, transactions: _transactions);
      expect(l1, equals(l2));
    });

    test('FinanceError equality works correctly', () {
      const e1 = FinanceError('some error');
      const e2 = FinanceError('some error');
      expect(e1, equals(e2));
    });
  });
}
