import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'package:unionflow_mobile_apps/features/onboarding/bloc/onboarding_bloc.dart';
import 'package:unionflow_mobile_apps/features/onboarding/data/datasources/souscription_datasource.dart';
import 'package:unionflow_mobile_apps/features/onboarding/data/models/formule_model.dart';
import 'package:unionflow_mobile_apps/features/onboarding/data/models/souscription_status_model.dart';

@GenerateMocks([SouscriptionDatasource])
import 'onboarding_bloc_test.mocks.dart';

// ─── Fixtures ────────────────────────────────────────────────────────────────

List<FormuleModel> _formules() => [
      const FormuleModel(
        code: 'BASIC',
        libelle: 'Basic',
        plage: 'PETITE',
        plageLibelle: '1-20 membres',
        minMembres: 1,
        maxMembres: 20,
        prixMensuel: 5000,
        prixAnnuel: 50000,
        ordreAffichage: 1,
      ),
      const FormuleModel(
        code: 'STANDARD',
        libelle: 'Standard',
        plage: 'MOYENNE',
        plageLibelle: '21-100 membres',
        minMembres: 21,
        maxMembres: 100,
        prixMensuel: 15000,
        prixAnnuel: 150000,
        ordreAffichage: 2,
      ),
    ];

SouscriptionStatusModel _souscription({String? waveLaunchUrl}) =>
    SouscriptionStatusModel(
      souscriptionId: 'sosc-1',
      statutValidation: 'EN_ATTENTE_PAIEMENT',
      typeFormule: 'BASIC',
      plageMembres: 'PETITE',
      plageLibelle: '1-20 membres',
      typePeriode: 'MENSUEL',
      typeOrganisation: 'ASSOCIATION',
      montantTotal: 5000,
      waveLaunchUrl: waveLaunchUrl,
      organisationId: 'org-1',
    );

// ─── Tests ───────────────────────────────────────────────────────────────────

void main() {
  late OnboardingBloc bloc;
  late MockSouscriptionDatasource mockDatasource;

  setUp(() {
    mockDatasource = MockSouscriptionDatasource();
    bloc = OnboardingBloc(mockDatasource);
  });

  tearDown(() => bloc.close());

  // ─── Initial state ──────────────────────────────────────────────────────────

  test('initial state is OnboardingInitial', () {
    expect(bloc.state, isA<OnboardingInitial>());
  });

  // ─── OnboardingStarted ──────────────────────────────────────────────────────

  group('OnboardingStarted — NO_SUBSCRIPTION', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingStepFormule] with loaded formulas',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        return bloc;
      },
      act: (b) => b.add(const OnboardingStarted(
        initialState: 'NO_SUBSCRIPTION',
        typeOrganisation: 'ASSOCIATION',
        organisationId: 'org-1',
      )),
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepFormule>()
            .having((s) => s.formules.length, 'formules count', 2),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingError] when getFormules throws',
      build: () {
        when(mockDatasource.getFormules())
            .thenThrow(Exception('Network error'));
        return bloc;
      },
      act: (b) => b.add(const OnboardingStarted(initialState: 'NO_SUBSCRIPTION')),
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingError>().having(
          (s) => s.message,
          'message',
          contains('Impossible de charger'),
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'falls back to OnboardingStepFormule for unknown initialState',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        return bloc;
      },
      act: (b) => b.add(const OnboardingStarted(initialState: 'UNKNOWN_STATE')),
      expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepFormule>()],
    );
  });

  group('OnboardingStarted — AWAITING_PAYMENT', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingStepSummary when getMaSouscription returns a souscription',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => _souscription());
        return bloc;
      },
      act: (b) => b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT')),
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepSummary>().having(
          (s) => s.souscription.souscriptionId,
          'souscriptionId',
          'sosc-1',
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'falls back to OnboardingStepFormule when getMaSouscription returns null',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) => b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT')),
      expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepFormule>()],
    );
  });

  group('OnboardingStarted — PAYMENT_INITIATED', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingStepPaiement when souscription has waveLaunchUrl',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription()).thenAnswer(
          (_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/xyz'),
        );
        return bloc;
      },
      act: (b) =>
          b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED')),
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepPaiement>()
            .having((s) => s.waveLaunchUrl, 'waveLaunchUrl', 'https://wave.com/pay/xyz'),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingStepAttente when souscription has no waveLaunchUrl',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => _souscription()); // no waveLaunchUrl
        return bloc;
      },
      act: (b) =>
          b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED')),
      expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepAttente>()],
    );
  });

  group('OnboardingStarted — AWAITING_VALIDATION', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingStepAttente with the fetched souscription',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => _souscription());
        return bloc;
      },
      act: (b) =>
          b.add(const OnboardingStarted(initialState: 'AWAITING_VALIDATION')),
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepAttente>().having(
          (s) => s.souscription?.souscriptionId,
          'souscriptionId',
          'sosc-1',
        ),
      ],
    );
  });

  group('OnboardingStarted — VALIDATED', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingStepAttente for edge case where activation not yet effective',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => _souscription());
        return bloc;
      },
      act: (b) => b.add(const OnboardingStarted(initialState: 'VALIDATED')),
      expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepAttente>()],
    );
  });

  group('OnboardingStarted — REJECTED', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingRejected with commentaire from statutValidation',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription()).thenAnswer((_) async =>
            SouscriptionStatusModel(
              souscriptionId: 'sosc-2',
              statutValidation: 'Documents manquants',
              typeFormule: 'BASIC',
              plageMembres: 'PETITE',
              plageLibelle: '1-20',
              typePeriode: 'MENSUEL',
              typeOrganisation: 'ASSOCIATION',
              organisationId: 'org-1',
            ));
        return bloc;
      },
      act: (b) => b.add(const OnboardingStarted(initialState: 'REJECTED')),
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingRejected>()
            .having((s) => s.commentaire, 'commentaire', 'Documents manquants'),
      ],
    );
  });

  // ─── OnboardingFormuleSelected ─────────────────────────────────────────────

  group('OnboardingFormuleSelected', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingStepPeriode with selected formule and plage',
      build: () {
        // Pre-load formulas by starting first
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        return bloc;
      },
      seed: () => OnboardingStepFormule(_formules()),
      act: (b) => b.add(const OnboardingFormuleSelected(
        codeFormule: 'BASIC',
        plage: 'PETITE',
      )),
      expect: () => [
        isA<OnboardingStepPeriode>()
            .having((s) => s.codeFormule, 'codeFormule', 'BASIC')
            .having((s) => s.plage, 'plage', 'PETITE'),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingStepPeriode with STANDARD/GRANDE',
      build: () => bloc,
      seed: () => OnboardingStepFormule(_formules()),
      act: (b) => b.add(const OnboardingFormuleSelected(
        codeFormule: 'STANDARD',
        plage: 'GRANDE',
      )),
      expect: () => [
        isA<OnboardingStepPeriode>()
            .having((s) => s.codeFormule, 'codeFormule', 'STANDARD')
            .having((s) => s.plage, 'plage', 'GRANDE'),
      ],
    );
  });

  // ─── OnboardingPeriodeSelected ─────────────────────────────────────────────

  group('OnboardingPeriodeSelected', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'does not emit a new state (only updates internal data)',
      build: () => bloc,
      act: (b) => b.add(const OnboardingPeriodeSelected(
        typePeriode: 'MENSUEL',
        typeOrganisation: 'ASSOCIATION',
        organisationId: 'org-1',
      )),
      expect: () => [],
    );
  });

  // ─── OnboardingDemandeConfirmee ────────────────────────────────────────────

  group('OnboardingDemandeConfirmee', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingError when required data is missing',
      build: () => bloc,
      // No formule, plage, periode, organisationId set → missing data
      act: (b) => b.add(const OnboardingDemandeConfirmee()),
      expect: () => [
        isA<OnboardingError>().having(
          (s) => s.message,
          'message',
          contains('Données manquantes'),
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingStepSummary] on successful creation',
      build: () {
        when(mockDatasource.creerDemande(
          typeFormule: anyNamed('typeFormule'),
          plageMembres: anyNamed('plageMembres'),
          typePeriode: anyNamed('typePeriode'),
          typeOrganisation: anyNamed('typeOrganisation'),
          organisationId: anyNamed('organisationId'),
        )).thenAnswer((_) async => _souscription());
        return bloc;
      },
      // Seed internal state: first select formule, then periode
      act: (b) async {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        b.add(const OnboardingStarted(
          initialState: 'NO_SUBSCRIPTION',
          typeOrganisation: 'ASSOCIATION',
          organisationId: 'org-1',
        ));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingFormuleSelected(
          codeFormule: 'BASIC',
          plage: 'PETITE',
        ));
        b.add(const OnboardingPeriodeSelected(
          typePeriode: 'MENSUEL',
          typeOrganisation: 'ASSOCIATION',
          organisationId: 'org-1',
        ));
        b.add(const OnboardingDemandeConfirmee());
      },
      expect: () => [
        isA<OnboardingLoading>(),     // from OnboardingStarted
        isA<OnboardingStepFormule>(), // after getFormules
        isA<OnboardingStepPeriode>(), // after OnboardingFormuleSelected
        // OnboardingPeriodeSelected emits nothing
        isA<OnboardingLoading>(),     // from OnboardingDemandeConfirmee
        isA<OnboardingStepSummary>()
            .having((s) => s.souscription.souscriptionId, 'id', 'sosc-1'),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingError] when creerDemande returns null',
      build: () {
        when(mockDatasource.creerDemande(
          typeFormule: anyNamed('typeFormule'),
          plageMembres: anyNamed('plageMembres'),
          typePeriode: anyNamed('typePeriode'),
          typeOrganisation: anyNamed('typeOrganisation'),
          organisationId: anyNamed('organisationId'),
        )).thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) async {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        b.add(const OnboardingStarted(
          initialState: 'NO_SUBSCRIPTION',
          typeOrganisation: 'ASSOCIATION',
          organisationId: 'org-1',
        ));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingFormuleSelected(
          codeFormule: 'BASIC',
          plage: 'PETITE',
        ));
        b.add(const OnboardingPeriodeSelected(
          typePeriode: 'MENSUEL',
          typeOrganisation: 'ASSOCIATION',
          organisationId: 'org-1',
        ));
        b.add(const OnboardingDemandeConfirmee());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepFormule>(),
        isA<OnboardingStepPeriode>(),
        isA<OnboardingLoading>(),
        isA<OnboardingError>().having(
          (s) => s.message,
          'message',
          contains('Erreur lors de la création'),
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingError] when creerDemande throws',
      build: () {
        when(mockDatasource.creerDemande(
          typeFormule: anyNamed('typeFormule'),
          plageMembres: anyNamed('plageMembres'),
          typePeriode: anyNamed('typePeriode'),
          typeOrganisation: anyNamed('typeOrganisation'),
          organisationId: anyNamed('organisationId'),
        )).thenThrow(Exception('API unavailable'));
        return bloc;
      },
      act: (b) async {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        b.add(const OnboardingStarted(
          initialState: 'NO_SUBSCRIPTION',
          typeOrganisation: 'ASSOCIATION',
          organisationId: 'org-1',
        ));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingFormuleSelected(
          codeFormule: 'BASIC',
          plage: 'PETITE',
        ));
        b.add(const OnboardingPeriodeSelected(
          typePeriode: 'MENSUEL',
          typeOrganisation: 'ASSOCIATION',
          organisationId: 'org-1',
        ));
        b.add(const OnboardingDemandeConfirmee());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepFormule>(),
        isA<OnboardingStepPeriode>(),
        isA<OnboardingLoading>(),
        isA<OnboardingError>(),
      ],
    );
  });

  // ─── OnboardingChoixPaiementOuvert ─────────────────────────────────────────

  group('OnboardingChoixPaiementOuvert', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingStepChoixPaiement when souscription is loaded',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => _souscription());
        return bloc;
      },
      act: (b) async {
        b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingChoixPaiementOuvert());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepSummary>(),
        isA<OnboardingStepChoixPaiement>().having(
          (s) => s.souscription.souscriptionId,
          'souscriptionId',
          'sosc-1',
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'does nothing when no souscription is loaded',
      build: () => bloc,
      act: (b) => b.add(const OnboardingChoixPaiementOuvert()),
      expect: () => [],
    );
  });

  // ─── OnboardingPaiementInitie ──────────────────────────────────────────────

  group('OnboardingPaiementInitie', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits OnboardingError when no souscription loaded',
      build: () => bloc,
      act: (b) => b.add(const OnboardingPaiementInitie()),
      expect: () => [
        isA<OnboardingError>().having(
          (s) => s.message,
          'message',
          contains('Souscription introuvable'),
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingStepPaiement] when initierPaiement succeeds',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => _souscription());
        when(mockDatasource.initierPaiement('sosc-1')).thenAnswer(
          (_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
        );
        return bloc;
      },
      act: (b) async {
        b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingPaiementInitie());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepSummary>(),
        isA<OnboardingLoading>(),
        isA<OnboardingStepPaiement>()
            .having((s) => s.waveLaunchUrl, 'waveLaunchUrl', 'https://wave.com/pay/abc'),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingError] when initierPaiement returns null',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => _souscription());
        when(mockDatasource.initierPaiement('sosc-1'))
            .thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) async {
        b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingPaiementInitie());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepSummary>(),
        isA<OnboardingLoading>(),
        isA<OnboardingError>().having(
          (s) => s.message,
          'message',
          contains('paiement Wave'),
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingError] when initierPaiement throws',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async => _souscription());
        when(mockDatasource.initierPaiement('sosc-1'))
            .thenThrow(Exception('Wave timeout'));
        return bloc;
      },
      act: (b) async {
        b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingPaiementInitie());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepSummary>(),
        isA<OnboardingLoading>(),
        isA<OnboardingError>(),
      ],
    );
  });

  // ─── OnboardingRetourDepuisWave ────────────────────────────────────────────

  group('OnboardingRetourDepuisWave', () {
    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingError] when no souscription loaded',
      build: () => bloc,
      act: (b) => b.add(const OnboardingRetourDepuisWave()),
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingError>().having(
          (s) => s.message,
          'message',
          contains('Souscription introuvable'),
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingPaiementConfirme] when confirmerPaiement returns true',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription())
            .thenAnswer((_) async =>
                _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'));
        when(mockDatasource.initierPaiement('sosc-1')).thenAnswer(
          (_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
        );
        when(mockDatasource.confirmerPaiement('sosc-1'))
            .thenAnswer((_) async => true);
        return bloc;
      },
      act: (b) async {
        b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingRetourDepuisWave());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepPaiement>(),
        isA<OnboardingLoading>(),
        isA<OnboardingPaiementConfirme>(),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingPaiementEchoue] when confirmerPaiement returns false',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription()).thenAnswer(
          (_) async =>
              _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
        );
        when(mockDatasource.confirmerPaiement('sosc-1'))
            .thenAnswer((_) async => false);
        return bloc;
      },
      act: (b) async {
        b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingRetourDepuisWave());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepPaiement>(),
        isA<OnboardingLoading>(),
        isA<OnboardingPaiementEchoue>().having(
          (s) => s.message,
          'message',
          contains('confirmé'),
        ),
      ],
    );

    blocTest<OnboardingBloc, OnboardingState>(
      'emits [OnboardingLoading, OnboardingPaiementEchoue] when confirmerPaiement throws',
      build: () {
        when(mockDatasource.getFormules())
            .thenAnswer((_) async => _formules());
        when(mockDatasource.getMaSouscription()).thenAnswer(
          (_) async =>
              _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
        );
        when(mockDatasource.confirmerPaiement('sosc-1'))
            .thenThrow(Exception('Timeout'));
        return bloc;
      },
      act: (b) async {
        b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
        await Future.delayed(const Duration(milliseconds: 50));
        b.add(const OnboardingRetourDepuisWave());
      },
      expect: () => [
        isA<OnboardingLoading>(),
        isA<OnboardingStepPaiement>(),
        isA<OnboardingLoading>(),
        isA<OnboardingPaiementEchoue>().having(
          (s) => s.message,
          'message',
          contains('Erreur lors de la confirmation'),
        ),
      ],
    );
  });

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

  group('OnboardingState equality', () {
    test('OnboardingError with same message are equal', () {
      const s1 = OnboardingError('err');
      const s2 = OnboardingError('err');
      expect(s1, equals(s2));
    });

    test('OnboardingStepFormule props contains formulas list', () {
      final formulas = _formules();
      final state = OnboardingStepFormule(formulas);
      expect(state.props, contains(formulas));
    });

    test('OnboardingStepPeriode props are correct', () {
      final formulas = _formules();
      const state = OnboardingStepPeriode(
        codeFormule: 'BASIC',
        plage: 'PETITE',
        formules: [],
      );
      expect(state.codeFormule, 'BASIC');
      expect(state.plage, 'PETITE');
    });

    test('OnboardingPaiementEchoue props are correct', () {
      final sosc = _souscription();
      final state = OnboardingPaiementEchoue(
        message: 'Error',
        souscription: sosc,
        waveLaunchUrl: 'https://wave.com',
      );
      expect(state.props,
          containsAll(['Error', sosc, 'https://wave.com']));
    });
  });
}
