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/budget.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/create_budget.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budget_by_id.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budget_tracking.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budgets.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/budget_bloc.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/budget_event.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/budget_state.dart';

@GenerateMocks([
  GetBudgets,
  GetBudgetById,
  CreateBudget,
  GetBudgetTracking,
])
import 'budget_bloc_test.mocks.dart';

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

final _now = DateTime(2026, 4, 20);

Budget _buildBudget({
  String id = 'budget-1',
  BudgetStatus status = BudgetStatus.active,
  String orgId = 'org-1',
  double totalPlanned = 100000,
  double totalRealized = 60000,
}) =>
    Budget(
      id: id,
      name: 'Budget Avril 2026',
      organizationId: orgId,
      period: BudgetPeriod.monthly,
      year: 2026,
      month: 4,
      status: status,
      totalPlanned: totalPlanned,
      totalRealized: totalRealized,
      createdBy: 'user-1',
      createdAt: _now,
      startDate: DateTime(2026, 4, 1),
      endDate: DateTime(2026, 4, 30),
    );

final _budget = _buildBudget();

const _budgetLine = BudgetLine(
  id: 'line-1',
  category: BudgetCategory.contributions,
  name: 'Cotisations membres',
  amountPlanned: 50000,
);

final _tracking = <String, dynamic>{
  'totalPlanned': 100000,
  'totalRealized': 60000,
  'realizationRate': 60.0,
};

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

void main() {
  late MockGetBudgets mockGetBudgets;
  late MockGetBudgetById mockGetBudgetById;
  late MockCreateBudget mockCreateBudget;
  late MockGetBudgetTracking mockGetBudgetTracking;

  setUp(() {
    mockGetBudgets = MockGetBudgets();
    mockGetBudgetById = MockGetBudgetById();
    mockCreateBudget = MockCreateBudget();
    mockGetBudgetTracking = MockGetBudgetTracking();
  });

  BudgetBloc buildBloc() => BudgetBloc(
        getBudgets: mockGetBudgets,
        getBudgetById: mockGetBudgetById,
        createBudget: mockCreateBudget,
        getBudgetTracking: mockGetBudgetTracking,
      );

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

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

  // ─── LoadBudgets ─────────────────────────────────────────────────────────

  group('LoadBudgets', () {
    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetsLoaded] on success',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async => Right([_budget]));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadBudgets(organizationId: 'org-1'),
      ),
      expect: () => [
        const BudgetsLoading(),
        BudgetsLoaded(budgets: [_budget]),
      ],
      verify: (_) {
        verify(mockGetBudgets(
          organizationId: 'org-1',
          status: null,
          year: null,
        )).called(1);
      },
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetsEmpty] when list is empty',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async => const Right([]));
        return buildBloc();
      },
      act: (b) => b.add(const LoadBudgets()),
      expect: () => [
        const BudgetsLoading(),
        const BudgetsEmpty(),
      ],
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetError] on failure',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async =>
            const Left(ServerFailure('server error')));
        return buildBloc();
      },
      act: (b) => b.add(const LoadBudgets(organizationId: 'org-1')),
      expect: () => [
        const BudgetsLoading(),
        const BudgetError('server error'),
      ],
    );

    blocTest<BudgetBloc, BudgetState>(
      'passes status and year filters to use case',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async => Right([_budget]));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadBudgets(
          organizationId: 'org-1',
          status: BudgetStatus.active,
          year: 2026,
        ),
      ),
      verify: (_) {
        verify(mockGetBudgets(
          organizationId: 'org-1',
          status: BudgetStatus.active,
          year: 2026,
        )).called(1);
      },
    );

    blocTest<BudgetBloc, BudgetState>(
      'BudgetsLoaded preserves filterStatus and filterYear',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async => Right([_budget]));
        return buildBloc();
      },
      act: (b) => b.add(
        const LoadBudgets(
          organizationId: 'org-1',
          status: BudgetStatus.draft,
          year: 2025,
        ),
      ),
      expect: () => [
        const BudgetsLoading(),
        BudgetsLoaded(
          budgets: [_budget],
          filterStatus: BudgetStatus.draft,
          filterYear: 2025,
        ),
      ],
    );
  });

  // ─── LoadBudgetById ───────────────────────────────────────────────────────

  group('LoadBudgetById', () {
    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetDetailLoaded] on success',
      build: () {
        when(mockGetBudgetById('budget-1'))
            .thenAnswer((_) async => Right(_budget));
        return buildBloc();
      },
      act: (b) => b.add(const LoadBudgetById('budget-1')),
      expect: () => [
        const BudgetsLoading(),
        BudgetDetailLoaded(_budget),
      ],
      verify: (_) {
        verify(mockGetBudgetById('budget-1')).called(1);
      },
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetError] when not found',
      build: () {
        when(mockGetBudgetById(any))
            .thenAnswer((_) async =>
                const Left(NotFoundFailure('Budget introuvable')));
        return buildBloc();
      },
      act: (b) => b.add(const LoadBudgetById('unknown')),
      expect: () => [
        const BudgetsLoading(),
        const BudgetError('Budget introuvable'),
      ],
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetError] on network failure',
      build: () {
        when(mockGetBudgetById(any))
            .thenAnswer((_) async => const Left(NetworkFailure('net')));
        return buildBloc();
      },
      act: (b) => b.add(const LoadBudgetById('budget-1')),
      expect: () => [
        const BudgetsLoading(),
        const BudgetError('net'),
      ],
    );
  });

  // ─── CreateBudgetEvent ────────────────────────────────────────────────────

  group('CreateBudgetEvent', () {
    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetActionInProgress(create), BudgetCreated] on success',
      build: () {
        when(mockCreateBudget(
          name: anyNamed('name'),
          description: anyNamed('description'),
          organizationId: anyNamed('organizationId'),
          period: anyNamed('period'),
          year: anyNamed('year'),
          month: anyNamed('month'),
          lines: anyNamed('lines'),
        )).thenAnswer((_) async => Right(_budget));
        return buildBloc();
      },
      act: (b) => b.add(
        const CreateBudgetEvent(
          name: 'Budget Avril 2026',
          organizationId: 'org-1',
          period: BudgetPeriod.monthly,
          year: 2026,
          month: 4,
          lines: [_budgetLine],
        ),
      ),
      expect: () => [
        const BudgetActionInProgress('create'),
        BudgetCreated(budget: _budget),
      ],
      verify: (_) {
        verify(mockCreateBudget(
          name: 'Budget Avril 2026',
          description: null,
          organizationId: 'org-1',
          period: BudgetPeriod.monthly,
          year: 2026,
          month: 4,
          lines: const [_budgetLine],
        )).called(1);
      },
    );

    blocTest<BudgetBloc, BudgetState>(
      'BudgetCreated has correct default message',
      build: () {
        when(mockCreateBudget(
          name: anyNamed('name'),
          description: anyNamed('description'),
          organizationId: anyNamed('organizationId'),
          period: anyNamed('period'),
          year: anyNamed('year'),
          month: anyNamed('month'),
          lines: anyNamed('lines'),
        )).thenAnswer((_) async => Right(_budget));
        return buildBloc();
      },
      act: (b) => b.add(
        const CreateBudgetEvent(
          name: 'Budget',
          organizationId: 'org-1',
          period: BudgetPeriod.annual,
          year: 2026,
          lines: [_budgetLine],
        ),
      ),
      expect: () => [
        const BudgetActionInProgress('create'),
        predicate<BudgetState>(
          (s) => s is BudgetCreated && s.message == 'Budget créé avec succès',
          'BudgetCreated with correct message',
        ),
      ],
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetActionInProgress(create), BudgetError] on validation failure',
      build: () {
        when(mockCreateBudget(
          name: anyNamed('name'),
          description: anyNamed('description'),
          organizationId: anyNamed('organizationId'),
          period: anyNamed('period'),
          year: anyNamed('year'),
          month: anyNamed('month'),
          lines: anyNamed('lines'),
        )).thenAnswer((_) async =>
            const Left(ValidationFailure('Nom du budget requis')));
        return buildBloc();
      },
      act: (b) => b.add(
        const CreateBudgetEvent(
          name: '',
          organizationId: 'org-1',
          period: BudgetPeriod.monthly,
          year: 2026,
          month: 4,
          lines: [_budgetLine],
        ),
      ),
      expect: () => [
        const BudgetActionInProgress('create'),
        const BudgetError('Nom du budget requis'),
      ],
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetActionInProgress(create), BudgetError] on server failure',
      build: () {
        when(mockCreateBudget(
          name: anyNamed('name'),
          description: anyNamed('description'),
          organizationId: anyNamed('organizationId'),
          period: anyNamed('period'),
          year: anyNamed('year'),
          month: anyNamed('month'),
          lines: anyNamed('lines'),
        )).thenAnswer((_) async =>
            const Left(ServerFailure('server error')));
        return buildBloc();
      },
      act: (b) => b.add(
        const CreateBudgetEvent(
          name: 'Budget valide',
          organizationId: 'org-1',
          period: BudgetPeriod.annual,
          year: 2026,
          lines: [_budgetLine],
        ),
      ),
      expect: () => [
        const BudgetActionInProgress('create'),
        const BudgetError('server error'),
      ],
    );
  });

  // ─── LoadBudgetTracking ───────────────────────────────────────────────────

  group('LoadBudgetTracking', () {
    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetTrackingLoaded] on success',
      build: () {
        when(mockGetBudgetById('budget-1'))
            .thenAnswer((_) async => Right(_budget));
        when(mockGetBudgetTracking(budgetId: 'budget-1'))
            .thenAnswer((_) async => Right(_tracking));
        return buildBloc();
      },
      act: (b) => b.add(const LoadBudgetTracking('budget-1')),
      expect: () => [
        const BudgetsLoading(),
        BudgetTrackingLoaded(budget: _budget, tracking: _tracking),
      ],
      verify: (_) {
        verify(mockGetBudgetById('budget-1')).called(1);
        verify(mockGetBudgetTracking(budgetId: 'budget-1')).called(1);
      },
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetError] when getBudgetById fails',
      build: () {
        when(mockGetBudgetById(any))
            .thenAnswer((_) async =>
                const Left(NotFoundFailure('Budget introuvable')));
        return buildBloc();
      },
      act: (b) => b.add(const LoadBudgetTracking('unknown')),
      expect: () => [
        const BudgetsLoading(),
        const BudgetError('Budget introuvable'),
      ],
      verify: (_) {
        // Tracking should NOT be called if budget fetch fails
        verifyNever(mockGetBudgetTracking(budgetId: anyNamed('budgetId')));
      },
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetError] when getTracking fails',
      build: () {
        when(mockGetBudgetById('budget-1'))
            .thenAnswer((_) async => Right(_budget));
        when(mockGetBudgetTracking(budgetId: anyNamed('budgetId')))
            .thenAnswer((_) async =>
                const Left(ServerFailure('tracking error')));
        return buildBloc();
      },
      act: (b) => b.add(const LoadBudgetTracking('budget-1')),
      expect: () => [
        const BudgetsLoading(),
        const BudgetError('tracking error'),
      ],
    );
  });

  // ─── RefreshBudgets ───────────────────────────────────────────────────────

  group('RefreshBudgets', () {
    blocTest<BudgetBloc, BudgetState>(
      'emits BudgetsLoaded without BudgetsLoading during refresh',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async => Right([_budget]));
        return buildBloc();
      },
      act: (b) =>
          b.add(const RefreshBudgets(organizationId: 'org-1')),
      // RefreshBudgets does NOT emit BudgetsLoading — goes straight to result
      expect: () => [
        BudgetsLoaded(budgets: [_budget]),
      ],
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits BudgetsEmpty on refresh when list is empty',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async => const Right([]));
        return buildBloc();
      },
      act: (b) => b.add(const RefreshBudgets()),
      expect: () => [const BudgetsEmpty()],
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits BudgetError on refresh failure',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async =>
            const Left(ServerFailure('refresh error')));
        return buildBloc();
      },
      act: (b) =>
          b.add(const RefreshBudgets(organizationId: 'org-1')),
      expect: () => [const BudgetError('refresh error')],
    );
  });

  // ─── FilterBudgets ────────────────────────────────────────────────────────

  group('FilterBudgets', () {
    final activeBudget =
        _buildBudget(id: 'budget-active', status: BudgetStatus.active);
    final draftBudget =
        _buildBudget(id: 'budget-draft', status: BudgetStatus.draft);

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetsLoaded] with filter applied',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: BudgetStatus.active,
          year: anyNamed('year'),
        )).thenAnswer((_) async => Right([activeBudget]));
        return buildBloc();
      },
      act: (b) => b.add(
        const FilterBudgets(status: BudgetStatus.active),
      ),
      expect: () => [
        const BudgetsLoading(),
        BudgetsLoaded(
          budgets: [activeBudget],
          filterStatus: BudgetStatus.active,
        ),
      ],
      verify: (_) {
        verify(mockGetBudgets(
          organizationId: null, // no loaded state, so null
          status: BudgetStatus.active,
          year: null,
        )).called(1);
      },
    );

    blocTest<BudgetBloc, BudgetState>(
      'uses organizationId from loaded state for filter',
      build: () {
        // First call (LoadBudgets) returns activeBudget with orgId='org-1'
        when(mockGetBudgets(
          organizationId: 'org-1',
          status: null,
          year: null,
        )).thenAnswer((_) async => Right([activeBudget]));
        // Second call (FilterBudgets) returns draftBudget
        when(mockGetBudgets(
          organizationId: 'org-1',
          status: BudgetStatus.draft,
          year: null,
        )).thenAnswer((_) async => Right([draftBudget]));
        return buildBloc();
      },
      act: (b) async {
        b.add(const LoadBudgets(organizationId: 'org-1'));
        // Wait until BudgetsLoaded so FilterBudgets sees org-1 in state
        await b.stream.firstWhere((s) => s is BudgetsLoaded);
        b.add(const FilterBudgets(status: BudgetStatus.draft));
      },
      expect: () => [
        const BudgetsLoading(),
        BudgetsLoaded(budgets: [activeBudget]),
        const BudgetsLoading(),
        BudgetsLoaded(
          budgets: [draftBudget],
          filterStatus: BudgetStatus.draft,
        ),
      ],
      verify: (_) {
        // organizationId extracted from first budget in loaded state
        verify(mockGetBudgets(
          organizationId: 'org-1',
          status: BudgetStatus.draft,
          year: null,
        )).called(1);
      },
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetsEmpty] when filter returns no results',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async => const Right([]));
        return buildBloc();
      },
      act: (b) =>
          b.add(const FilterBudgets(status: BudgetStatus.cancelled)),
      expect: () => [
        const BudgetsLoading(),
        const BudgetsEmpty(),
      ],
    );

    blocTest<BudgetBloc, BudgetState>(
      'emits [BudgetsLoading, BudgetError] on filter failure',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async =>
            const Left(ServerFailure('filter error')));
        return buildBloc();
      },
      act: (b) => b.add(const FilterBudgets(year: 2025)),
      expect: () => [
        const BudgetsLoading(),
        const BudgetError('filter error'),
      ],
    );

    blocTest<BudgetBloc, BudgetState>(
      'filters with year only',
      build: () {
        when(mockGetBudgets(
          organizationId: anyNamed('organizationId'),
          status: anyNamed('status'),
          year: anyNamed('year'),
        )).thenAnswer((_) async => Right([_budget]));
        return buildBloc();
      },
      act: (b) => b.add(const FilterBudgets(year: 2026)),
      verify: (_) {
        verify(mockGetBudgets(
          organizationId: null,
          status: null,
          year: 2026,
        )).called(1);
      },
    );
  });

  // ─── State equality / Budget entity ──────────────────────────────────────

  group('Budget entity computations', () {
    test('realizationRate is correct', () {
      expect(_budget.realizationRate, closeTo(60.0, 0.01));
    });

    test('variance is correct', () {
      expect(_budget.variance, closeTo(-40000, 0.01));
    });

    test('isOverBudget is false when realized < planned', () {
      expect(_budget.isOverBudget, isFalse);
    });

    test('isOverBudget is true when realized > planned', () {
      final over = _buildBudget(totalPlanned: 100000, totalRealized: 120000);
      expect(over.isOverBudget, isTrue);
    });

    test('isActive is true for active budget', () {
      expect(_budget.isActive, isTrue);
    });

    test('realizationRate is 0 when totalPlanned is 0', () {
      final zeroBudget = _buildBudget(totalPlanned: 0, totalRealized: 0);
      expect(zeroBudget.realizationRate, equals(0.0));
    });

    test('varianceRate is 0 when totalPlanned is 0', () {
      final zeroBudget = _buildBudget(totalPlanned: 0, totalRealized: 0);
      expect(zeroBudget.varianceRate, equals(0.0));
    });
  });

  group('BudgetLine entity computations', () {
    test('realizationRate is correct', () {
      const line = BudgetLine(
        id: 'l',
        category: BudgetCategory.events,
        name: 'Test',
        amountPlanned: 10000,
        amountRealized: 7500,
      );
      expect(line.realizationRate, closeTo(75.0, 0.01));
    });

    test('isOverBudget is false when realized <= planned', () {
      expect(_budgetLine.isOverBudget, isFalse);
    });

    test('realizationRate is 0 when amountPlanned is 0', () {
      const line = BudgetLine(
        id: 'l',
        category: BudgetCategory.other,
        name: 'Zero',
        amountPlanned: 0,
      );
      expect(line.realizationRate, equals(0.0));
    });
  });

  group('State equality', () {
    test('BudgetsLoaded equality', () {
      final s1 = BudgetsLoaded(budgets: [_budget]);
      final s2 = BudgetsLoaded(budgets: [_budget]);
      expect(s1, equals(s2));
    });

    test('BudgetError equality', () {
      const e1 = BudgetError('msg');
      const e2 = BudgetError('msg');
      expect(e1, equals(e2));
    });

    test('BudgetsEmpty has default message', () {
      const s = BudgetsEmpty();
      expect(s.message, 'Aucun budget trouvé');
    });

    test('BudgetActionInProgress equality', () {
      expect(
        const BudgetActionInProgress('create'),
        equals(const BudgetActionInProgress('create')),
      );
    });
  });
}
