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/error/failures.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/approve_transaction.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_approval_by_id.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_pending_approvals.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/reject_transaction.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/approval_bloc.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/approval_event.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/approval_state.dart';

@GenerateMocks([
  GetPendingApprovals,
  GetApprovalById,
  ApproveTransaction,
  RejectTransaction,
])
import 'approval_bloc_test.mocks.dart';

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

TransactionApproval _buildApproval({
  String id = 'appr-1',
  ApprovalStatus status = ApprovalStatus.pending,
}) =>
    TransactionApproval(
      id: id,
      transactionId: 'tx-1',
      transactionType: TransactionType.contribution,
      amount: 50000,
      requesterId: 'user-1',
      requesterName: 'Alice Diallo',
      requiredLevel: ApprovalLevel.level1,
      status: status,
      createdAt: DateTime(2026, 4, 1),
    );

final _pendingApproval = _buildApproval();
final _approvedApproval =
    _buildApproval(status: ApprovalStatus.approved);
final _rejectedApproval =
    _buildApproval(status: ApprovalStatus.rejected);

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

void main() {
  late MockGetPendingApprovals mockGetPendingApprovals;
  late MockGetApprovalById mockGetApprovalById;
  late MockApproveTransaction mockApproveTransaction;
  late MockRejectTransaction mockRejectTransaction;

  setUp(() {
    mockGetPendingApprovals = MockGetPendingApprovals();
    mockGetApprovalById = MockGetApprovalById();
    mockApproveTransaction = MockApproveTransaction();
    mockRejectTransaction = MockRejectTransaction();
  });

  ApprovalBloc buildBloc() => ApprovalBloc(
        getPendingApprovals: mockGetPendingApprovals,
        getApprovalById: mockGetApprovalById,
        approveTransaction: mockApproveTransaction,
        rejectTransaction: mockRejectTransaction,
      );

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

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

  // ─── LoadPendingApprovals ────────────────────────────────────────────────

  group('LoadPendingApprovals', () {
    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalsLoading, ApprovalsLoaded] when approvals exist',
      build: () {
        when(mockGetPendingApprovals(organizationId: anyNamed('organizationId')))
            .thenAnswer((_) async => Right([_pendingApproval]));
        return buildBloc();
      },
      act: (b) =>
          b.add(const LoadPendingApprovals(organizationId: 'org-1')),
      expect: () => [
        const ApprovalsLoading(),
        ApprovalsLoaded(
          approvals: [_pendingApproval],
          pendingCount: 1,
        ),
      ],
      verify: (_) {
        verify(mockGetPendingApprovals(organizationId: 'org-1')).called(1);
      },
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalsLoading, ApprovalsLoaded] with correct pendingCount',
      build: () {
        when(mockGetPendingApprovals(organizationId: anyNamed('organizationId')))
            .thenAnswer((_) async => Right([
                  _pendingApproval,
                  _buildApproval(id: 'appr-2'),
                  _buildApproval(id: 'appr-3'),
                ]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadPendingApprovals()),
      expect: () => [
        const ApprovalsLoading(),
        predicate<ApprovalState>(
          (s) => s is ApprovalsLoaded && s.pendingCount == 3,
          'ApprovalsLoaded with pendingCount == 3',
        ),
      ],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalsLoading, ApprovalsEmpty] when list is empty',
      build: () {
        when(mockGetPendingApprovals(organizationId: anyNamed('organizationId')))
            .thenAnswer((_) async => const Right([]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadPendingApprovals(organizationId: 'org-1')),
      expect: () => [
        const ApprovalsLoading(),
        const ApprovalsEmpty(),
      ],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalsLoading, ApprovalError] on failure',
      build: () {
        when(mockGetPendingApprovals(organizationId: anyNamed('organizationId')))
            .thenAnswer((_) async =>
                const Left(ServerFailure('server error')));
        return buildBloc();
      },
      act: (b) => b.add(const LoadPendingApprovals(organizationId: 'org-1')),
      expect: () => [
        const ApprovalsLoading(),
        const ApprovalError('server error'),
      ],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'works without organizationId (null)',
      build: () {
        when(mockGetPendingApprovals(organizationId: null))
            .thenAnswer((_) async => Right([_pendingApproval]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadPendingApprovals()),
      expect: () => [
        const ApprovalsLoading(),
        isA<ApprovalsLoaded>(),
      ],
    );
  });

  // ─── LoadApprovalById ────────────────────────────────────────────────────

  group('LoadApprovalById', () {
    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalsLoading, ApprovalDetailLoaded] on success',
      build: () {
        when(mockGetApprovalById('appr-1'))
            .thenAnswer((_) async => Right(_pendingApproval));
        return buildBloc();
      },
      act: (b) => b.add(const LoadApprovalById('appr-1')),
      expect: () => [
        const ApprovalsLoading(),
        ApprovalDetailLoaded(_pendingApproval),
      ],
      verify: (_) {
        verify(mockGetApprovalById('appr-1')).called(1);
      },
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalsLoading, ApprovalError] when not found',
      build: () {
        when(mockGetApprovalById(any))
            .thenAnswer((_) async =>
                const Left(NotFoundFailure('Approbation introuvable')));
        return buildBloc();
      },
      act: (b) => b.add(const LoadApprovalById('unknown-id')),
      expect: () => [
        const ApprovalsLoading(),
        const ApprovalError('Approbation introuvable'),
      ],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalsLoading, ApprovalError] on network failure',
      build: () {
        when(mockGetApprovalById(any))
            .thenAnswer((_) async =>
                const Left(NetworkFailure('net')));
        return buildBloc();
      },
      act: (b) => b.add(const LoadApprovalById('appr-1')),
      expect: () => [
        const ApprovalsLoading(),
        const ApprovalError('net'),
      ],
    );
  });

  // ─── ApproveTransactionEvent ─────────────────────────────────────────────

  group('ApproveTransactionEvent', () {
    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalActionInProgress(approve), TransactionApproved] on success',
      build: () {
        when(mockApproveTransaction(
          approvalId: anyNamed('approvalId'),
          comment: anyNamed('comment'),
        )).thenAnswer((_) async => Right(_approvedApproval));
        return buildBloc();
      },
      act: (b) => b.add(
        const ApproveTransactionEvent(
          approvalId: 'appr-1',
          comment: 'OK pour moi',
        ),
      ),
      expect: () => [
        const ApprovalActionInProgress('approve'),
        TransactionApproved(approval: _approvedApproval),
      ],
      verify: (_) {
        verify(mockApproveTransaction(
          approvalId: 'appr-1',
          comment: 'OK pour moi',
        )).called(1);
      },
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalActionInProgress(approve), TransactionApproved] without comment',
      build: () {
        when(mockApproveTransaction(
          approvalId: anyNamed('approvalId'),
          comment: anyNamed('comment'),
        )).thenAnswer((_) async => Right(_approvedApproval));
        return buildBloc();
      },
      act: (b) => b.add(
        const ApproveTransactionEvent(approvalId: 'appr-1'),
      ),
      expect: () => [
        const ApprovalActionInProgress('approve'),
        isA<TransactionApproved>(),
      ],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalActionInProgress(approve), ApprovalError] on failure',
      build: () {
        when(mockApproveTransaction(
          approvalId: anyNamed('approvalId'),
          comment: anyNamed('comment'),
        )).thenAnswer(
            (_) async => const Left(ForbiddenFailure('Droits insuffisants')));
        return buildBloc();
      },
      act: (b) => b.add(
        const ApproveTransactionEvent(approvalId: 'appr-1'),
      ),
      expect: () => [
        const ApprovalActionInProgress('approve'),
        const ApprovalError('Droits insuffisants'),
      ],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'TransactionApproved has correct default message',
      build: () {
        when(mockApproveTransaction(
          approvalId: anyNamed('approvalId'),
          comment: anyNamed('comment'),
        )).thenAnswer((_) async => Right(_approvedApproval));
        return buildBloc();
      },
      act: (b) =>
          b.add(const ApproveTransactionEvent(approvalId: 'appr-1')),
      expect: () => [
        const ApprovalActionInProgress('approve'),
        predicate<ApprovalState>(
          (s) =>
              s is TransactionApproved &&
              s.message == 'Transaction approuvée avec succès',
          'TransactionApproved with correct message',
        ),
      ],
    );
  });

  // ─── RejectTransactionEvent ──────────────────────────────────────────────

  group('RejectTransactionEvent', () {
    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalActionInProgress(reject), TransactionRejected] on success',
      build: () {
        when(mockRejectTransaction(
          approvalId: anyNamed('approvalId'),
          reason: anyNamed('reason'),
        )).thenAnswer((_) async => Right(_rejectedApproval));
        return buildBloc();
      },
      act: (b) => b.add(
        const RejectTransactionEvent(
          approvalId: 'appr-1',
          reason: 'Montant incorrect',
        ),
      ),
      expect: () => [
        const ApprovalActionInProgress('reject'),
        TransactionRejected(approval: _rejectedApproval),
      ],
      verify: (_) {
        verify(mockRejectTransaction(
          approvalId: 'appr-1',
          reason: 'Montant incorrect',
        )).called(1);
      },
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits [ApprovalActionInProgress(reject), ApprovalError] on failure',
      build: () {
        when(mockRejectTransaction(
          approvalId: anyNamed('approvalId'),
          reason: anyNamed('reason'),
        )).thenAnswer((_) async =>
            const Left(ValidationFailure('ID approbation requis')));
        return buildBloc();
      },
      act: (b) => b.add(
        const RejectTransactionEvent(
          approvalId: 'appr-1',
          reason: 'bad data',
        ),
      ),
      expect: () => [
        const ApprovalActionInProgress('reject'),
        const ApprovalError('ID approbation requis'),
      ],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'TransactionRejected has correct default message',
      build: () {
        when(mockRejectTransaction(
          approvalId: anyNamed('approvalId'),
          reason: anyNamed('reason'),
        )).thenAnswer((_) async => Right(_rejectedApproval));
        return buildBloc();
      },
      act: (b) => b.add(
        const RejectTransactionEvent(
          approvalId: 'appr-1',
          reason: 'Raison valide',
        ),
      ),
      expect: () => [
        const ApprovalActionInProgress('reject'),
        predicate<ApprovalState>(
          (s) =>
              s is TransactionRejected &&
              s.message == 'Transaction rejetée avec succès',
          'TransactionRejected with correct message',
        ),
      ],
    );
  });

  // ─── RefreshApprovals ────────────────────────────────────────────────────

  group('RefreshApprovals', () {
    blocTest<ApprovalBloc, ApprovalState>(
      'emits ApprovalsLoaded without ApprovalsLoading during refresh',
      build: () {
        when(mockGetPendingApprovals(
          organizationId: anyNamed('organizationId'),
        )).thenAnswer((_) async => Right([_pendingApproval]));
        return buildBloc();
      },
      // No seed — run from ApprovalInitial. RefreshApprovals skips ApprovalsLoading.
      act: (b) => b.add(const RefreshApprovals(organizationId: 'org-1')),
      // No ApprovalsLoading emitted — goes straight to result
      expect: () => [
        ApprovalsLoaded(
          approvals: [_pendingApproval],
          pendingCount: 1,
        ),
      ],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits ApprovalsEmpty when refresh returns empty list',
      build: () {
        when(mockGetPendingApprovals(
          organizationId: anyNamed('organizationId'),
        )).thenAnswer((_) async => const Right([]));
        return buildBloc();
      },
      act: (b) => b.add(const RefreshApprovals()),
      expect: () => [const ApprovalsEmpty()],
    );

    blocTest<ApprovalBloc, ApprovalState>(
      'emits ApprovalError on refresh failure',
      build: () {
        when(mockGetPendingApprovals(
          organizationId: anyNamed('organizationId'),
        )).thenAnswer((_) async =>
            const Left(ServerFailure('serveur indisponible')));
        return buildBloc();
      },
      act: (b) => b.add(const RefreshApprovals()),
      expect: () => [const ApprovalError('serveur indisponible')],
    );
  });

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

  group('State equality / props', () {
    test('ApprovalsLoaded equality', () {
      final s1 = ApprovalsLoaded(
        approvals: [_pendingApproval],
        pendingCount: 1,
      );
      final s2 = ApprovalsLoaded(
        approvals: [_pendingApproval],
        pendingCount: 1,
      );
      expect(s1, equals(s2));
    });

    test('ApprovalActionInProgress equality', () {
      expect(
        const ApprovalActionInProgress('approve'),
        equals(const ApprovalActionInProgress('approve')),
      );
      expect(
        const ApprovalActionInProgress('approve'),
        isNot(equals(const ApprovalActionInProgress('reject'))),
      );
    });

    test('TransactionApproval.isPending is true for pending status', () {
      expect(_pendingApproval.isPending, isTrue);
    });

    test('TransactionApproval.isCompleted is false for pending', () {
      expect(_pendingApproval.isCompleted, isFalse);
    });

    test('TransactionApproval.requiredApprovals matches level', () {
      expect(_pendingApproval.requiredApprovals, equals(1)); // level1
    });

    test('ApprovalsEmpty has default message', () {
      const s = ApprovalsEmpty();
      expect(s.message, 'Aucune approbation en attente');
    });
  });
}
