package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.lang.reflect.Method;

import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.service.support.SecuriteHelper;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

/**
 * Tests complémentaires pour {@link CotisationService} — cas d'exception dans
 * {@code envoyerRappelsCotisationsGroupes} et valeurs null retournées par les requêtes
 * d'agrégat (cotisationsEnAttente, montantDu, totalPayeAnnee).
 *
 * <p>Stratégie pour les ternaires : {@code cotisationRepository.getEntityManager()}
 * est mocké pour retourner des {@link TypedQuery} dont {@code getSingleResult()} retourne {@code null}.
 */
@QuarkusTest
@DisplayName("CotisationService — cas d'exception et valeurs null des requêtes d'agrégat")
class CotisationServiceBranchesMissingTest {

    @Inject
    CotisationService cotisationService;

    @InjectMock
    CotisationRepository cotisationRepository;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    OrganisationRepository organisationRepository;

    @InjectMock
    SecuriteHelper securiteHelper;

    @InjectMock
    OrganisationService organisationService;

    // =========================================================================
    // L675-676 : catch(Exception) dans boucle envoyerRappelsCotisationsGroupes
    //
    // Code source (L665-677) :
    //   for (UUID membreId : membreIds) {
    //     try {
    //       List<Cotisation> cotisations = cotisationRepository
    //           .findCotisationsAuRappel(7, 3).stream()
    //           .filter(c -> c.getMembre() != null && c.getMembre().getId().equals(membreId))
    //           .collect(Collectors.toList());
    //       for (Cotisation cotisation : cotisations) { ... }
    //     } catch (Exception e) {           ← L675
    //       log.warn("Erreur ...");          ← L676
    //     }
    //   }
    //
    // Pour déclencher le catch : mock findCotisationsAuRappel pour lancer RuntimeException.
    // =========================================================================

    @Nested
    @DisplayName("L675-676 — catch(Exception) dans envoyerRappelsCotisationsGroupes")
    class EnvoyerRappelsCatchExceptionTest {

        /**
         * Couvre les lignes 675-676 de CotisationService :
         * {@code catch(Exception e) { log.warn(...) }}
         *
         * <p>En mockant {@code cotisationRepository.findCotisationsAuRappel} pour lever une
         * RuntimeException, on force l'entrée dans le bloc catch pour chaque itération de la boucle.
         * La méthode absorbe l'exception (catch interne) et retourne 0 rappels envoyés.
         */
        @Test
        @DisplayName("findCotisationsAuRappel lève RuntimeException → catch(Exception) atteint (L675-676), retourne 0")
        void envoyerRappels_findCotisationsLanceException_catchAtteint_retourne0() {
            // Mock : findCotisationsAuRappel lève une exception à chaque appel
            when(cotisationRepository.findCotisationsAuRappel(anyInt(), anyInt()))
                    .thenThrow(new RuntimeException("Erreur simulée base de données"));

            UUID membreId1 = UUID.randomUUID();
            UUID membreId2 = UUID.randomUUID();

            // La méthode doit absorber l'exception via le catch(Exception e) ligne 675
            // et retourner 0 (aucun rappel envoyé car exception attrapée pour chaque itération)
            int result = cotisationService.envoyerRappelsCotisationsGroupes(
                    List.of(membreId1, membreId2));

            // Aucune exception propagée hors du catch + 0 rappels envoyés
            assertThat(result).isEqualTo(0);
        }

        /**
         * Variante : un seul membre dans la liste, l'exception IllegalStateException est attrapée.
         * Couvre à nouveau L675-676 avec une liste de taille 1 et une exception différente.
         */
        @Test
        @DisplayName("Un seul membre, findCotisationsAuRappel lève IllegalStateException → catch L675, retourne 0")
        void envoyerRappels_unSeulMembre_illegalStateException_catchAtteintL676_retourne0() {
            when(cotisationRepository.findCotisationsAuRappel(anyInt(), anyInt()))
                    .thenThrow(new IllegalStateException("Base de données inaccessible"));

            int result = cotisationService.envoyerRappelsCotisationsGroupes(
                    List.of(UUID.randomUUID()));

            // Le log.warn de L676 a été exécuté, 0 rappels envoyés
            assertThat(result).isZero();
        }
    }

    // =========================================================================
    // L819-820, L822 — ternaires null dans getMesCotisationsSynthese (branche ADMIN)
    //
    // Dans la branche ADMIN (L796-824), les requêtes JPQL via EM retournent :
    //   - Long cotisationsEnAttente = em.createQuery("SELECT COUNT...", Long.class).getSingleResult()
    //   - BigDecimal montantDu = em.createQuery("SELECT COALESCE(SUM(...montantDu...)...", BigDecimal.class).getSingleResult()
    //   - BigDecimal totalPayeAnnee = em.createQuery("SELECT COALESCE(SUM(...montantPaye...)...", BigDecimal.class).getSingleResult()
    //
    // Si le mock retourne null pour ces valeurs, les branches "== null → fallback" sont couvertes.
    // =========================================================================

    @Nested
    @DisplayName("L819-820, L822 — ternaires null dans getMesCotisationsSynthese (branche ADMIN)")
    class GetMesCotisationsSyntheseAdminNullTest {

        /**
         * Couvre les lignes 819 (cotisationsEnAttente == null → 0) et 822 (totalPayeAnnee == null
         * → BigDecimal.ZERO) dans la branche ADMIN de getMesCotisationsSynthese.
         *
         * <p>On mock {@code cotisationRepository.getEntityManager()} pour retourner un
         * EntityManager Mockito. Les TypedQuery mockés retournent null pour forcer les branches.
         */
        @Test
        @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
        @DisplayName("ADMIN avec org : cotisationsEnAttente=null + totalPayeAnnee=null → fallback 0 et ZERO (L819, L822)")
        void syntheseAdmin_cotisationsEnAttenteEtTotalPayeNull_fallbackZero() {
            UUID orgId = UUID.randomUUID();
            Organisation org = new Organisation();
            org.setId(orgId);
            org.setNom("Org Admin Test Null");

            when(securiteHelper.resolveEmail()).thenReturn("admin@unionflow.com");
            when(securiteHelper.getRoles()).thenReturn(Set.of("ADMIN"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@unionflow.com"))
                    .thenReturn(List.of(org));

            // Mock EntityManager retourné par cotisationRepository.getEntityManager()
            EntityManager mockEm = mock(EntityManager.class);
            when(cotisationRepository.getEntityManager()).thenReturn(mockEm);

            // Query COUNT → cotisationsEnAttente = null (→ branche L819: false → 0)
            @SuppressWarnings("unchecked")
            TypedQuery<Long> countQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("COUNT"), eq(Long.class))).thenReturn(countQuery);
            when(countQuery.setParameter(anyString(), any())).thenReturn(countQuery);
            when(countQuery.getSingleResult()).thenReturn(null);

            // Query montantDu → retourne BigDecimal.ZERO (valeur normale, L820 branche true)
            @SuppressWarnings("unchecked")
            TypedQuery<BigDecimal> montantDuQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("montantDu"), eq(BigDecimal.class)))
                    .thenReturn(montantDuQuery);
            when(montantDuQuery.setParameter(anyString(), any())).thenReturn(montantDuQuery);
            when(montantDuQuery.getSingleResult()).thenReturn(BigDecimal.ZERO);

            // Query MIN prochaineEcheance → null (normal, pas de ternaire à couvrir)
            @SuppressWarnings("unchecked")
            TypedQuery<LocalDate> echeanceQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("MIN"), eq(LocalDate.class))).thenReturn(echeanceQuery);
            when(echeanceQuery.setParameter(anyString(), any())).thenReturn(echeanceQuery);
            when(echeanceQuery.getSingleResult()).thenReturn(null);

            // Query montantPaye (totalPayeAnnee) → null (→ branche L822: false → BigDecimal.ZERO)
            @SuppressWarnings("unchecked")
            TypedQuery<BigDecimal> totalPayeQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("montantPaye"), eq(BigDecimal.class)))
                    .thenReturn(totalPayeQuery);
            when(totalPayeQuery.setParameter(anyString(), any())).thenReturn(totalPayeQuery);
            when(totalPayeQuery.getSingleResult()).thenReturn(null);

            Map<String, Object> synthese = cotisationService.getMesCotisationsSynthese();

            assertThat(synthese).isNotNull();
            // L819 : cotisationsEnAttente == null → 0
            assertThat(((Number) synthese.get("cotisationsEnAttente")).intValue()).isEqualTo(0);
            // L820 : montantDu non null → BigDecimal.ZERO (valeur normale)
            assertThat((BigDecimal) synthese.get("montantDu")).isEqualByComparingTo(BigDecimal.ZERO);
            // L822 : totalPayeAnnee == null → BigDecimal.ZERO
            assertThat((BigDecimal) synthese.get("totalPayeAnnee"))
                    .isEqualByComparingTo(BigDecimal.ZERO);
        }

        /**
         * Couvre la ligne 820 (montantDu == null → BigDecimal.ZERO) dans la branche ADMIN.
         *
         * <p>Le COALESCE dans la requête devrait normalement garantir non-null, mais via le mock
         * on peut forcer null pour couvrir cette branche défensive.
         */
        @Test
        @TestSecurity(user = "admin2@unionflow.com", roles = {"ADMIN"})
        @DisplayName("ADMIN avec org : montantDu=null → fallback BigDecimal.ZERO (L820)")
        void syntheseAdmin_montantDuNull_fallbackZero() {
            UUID orgId = UUID.randomUUID();
            Organisation org = new Organisation();
            org.setId(orgId);
            org.setNom("Org Admin Montant Null");

            when(securiteHelper.resolveEmail()).thenReturn("admin2@unionflow.com");
            when(securiteHelper.getRoles()).thenReturn(Set.of("ADMIN"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin2@unionflow.com"))
                    .thenReturn(List.of(org));

            EntityManager mockEm = mock(EntityManager.class);
            when(cotisationRepository.getEntityManager()).thenReturn(mockEm);

            // COUNT retourne valeur non null
            @SuppressWarnings("unchecked")
            TypedQuery<Long> countQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("COUNT"), eq(Long.class))).thenReturn(countQuery);
            when(countQuery.setParameter(anyString(), any())).thenReturn(countQuery);
            when(countQuery.getSingleResult()).thenReturn(3L);

            // montantDu → null (→ L820: false → BigDecimal.ZERO)
            @SuppressWarnings("unchecked")
            TypedQuery<BigDecimal> montantDuQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("montantDu"), eq(BigDecimal.class)))
                    .thenReturn(montantDuQuery);
            when(montantDuQuery.setParameter(anyString(), any())).thenReturn(montantDuQuery);
            when(montantDuQuery.getSingleResult()).thenReturn(null);

            // MIN prochaineEcheance → null
            @SuppressWarnings("unchecked")
            TypedQuery<LocalDate> echeanceQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("MIN"), eq(LocalDate.class))).thenReturn(echeanceQuery);
            when(echeanceQuery.setParameter(anyString(), any())).thenReturn(echeanceQuery);
            when(echeanceQuery.getSingleResult()).thenReturn(null);

            // totalPayeAnnee → valeur normale
            @SuppressWarnings("unchecked")
            TypedQuery<BigDecimal> totalPayeQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("montantPaye"), eq(BigDecimal.class)))
                    .thenReturn(totalPayeQuery);
            when(totalPayeQuery.setParameter(anyString(), any())).thenReturn(totalPayeQuery);
            when(totalPayeQuery.getSingleResult()).thenReturn(BigDecimal.valueOf(5000));

            Map<String, Object> synthese = cotisationService.getMesCotisationsSynthese();

            assertThat(synthese).isNotNull();
            // L820 : montantDu == null → BigDecimal.ZERO
            assertThat((BigDecimal) synthese.get("montantDu")).isEqualByComparingTo(BigDecimal.ZERO);
            // cotisationsEnAttente non null → 3
            assertThat(((Number) synthese.get("cotisationsEnAttente")).intValue()).isEqualTo(3);
            // totalPayeAnnee non null → 5000
            assertThat((BigDecimal) synthese.get("totalPayeAnnee"))
                    .isEqualByComparingTo(BigDecimal.valueOf(5000));
        }
    }

    // =========================================================================
    // L865-866, L868 — ternaires null dans getMesCotisationsSynthese (branche MEMBRE)
    //
    // Dans la branche MEMBRE (L830-869), les requêtes JPQL via EM retournent :
    //   - Long cotisationsEnAttente = ...getSingleResult()
    //   - BigDecimal montantDu = ...getSingleResult()
    //   - BigDecimal totalPayeAnnee = ...getSingleResult()
    //
    // L865 : cotisationsEnAttente != null ? cotisationsEnAttente.intValue() : 0
    // L866 : montantDu != null ? montantDu : BigDecimal.ZERO
    // L868 : totalPayeAnnee != null ? totalPayeAnnee : BigDecimal.ZERO
    // =========================================================================

    @Nested
    @DisplayName("L865-866, L868 — ternaires null dans getMesCotisationsSynthese (branche MEMBRE)")
    class GetMesCotisationsSyntheseMembreNullTest {

        /**
         * Couvre les lignes 865 (cotisationsEnAttente == null → 0) et 868 (totalPayeAnnee == null
         * → BigDecimal.ZERO) dans la branche MEMBRE de getMesCotisationsSynthese.
         *
         * <p>Un MEMBRE connecté dont le mock COUNT retourne null et SUM montantPaye retourne null.
         */
        @Test
        @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
        @DisplayName("MEMBRE : cotisationsEnAttente=null + totalPayeAnnee=null → fallback 0 et ZERO (L865, L868)")
        void synthese_membre_cotisationsEnAttenteEtTotalPayeNull_fallback() {
            UUID membreId = UUID.randomUUID();
            Membre membre = new Membre();
            membre.setId(membreId);
            membre.setNumeroMembre("UF-TEST-NULL");
            membre.setEmail("membre@unionflow.com");
            membre.setActif(true);

            when(securiteHelper.resolveEmail()).thenReturn("membre@unionflow.com");
            when(securiteHelper.getRoles()).thenReturn(Set.of("MEMBRE"));
            when(membreRepository.findByEmail("membre@unionflow.com"))
                    .thenReturn(Optional.of(membre));

            EntityManager mockEm = mock(EntityManager.class);
            when(cotisationRepository.getEntityManager()).thenReturn(mockEm);

            // COUNT(c) → null (→ L865: cotisationsEnAttente == null → 0)
            @SuppressWarnings("unchecked")
            TypedQuery<Long> countQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("COUNT"), eq(Long.class))).thenReturn(countQuery);
            when(countQuery.setParameter(anyString(), any())).thenReturn(countQuery);
            when(countQuery.getSingleResult()).thenReturn(null);

            // SUM montantDu → BigDecimal.ZERO (valeur normale, L866 branche true)
            @SuppressWarnings("unchecked")
            TypedQuery<BigDecimal> montantDuQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("montantDu"), eq(BigDecimal.class)))
                    .thenReturn(montantDuQuery);
            when(montantDuQuery.setParameter(anyString(), any())).thenReturn(montantDuQuery);
            when(montantDuQuery.getSingleResult()).thenReturn(BigDecimal.ZERO);

            // MIN dateEcheance → null (normal)
            @SuppressWarnings("unchecked")
            TypedQuery<LocalDate> echeanceQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("MIN"), eq(LocalDate.class))).thenReturn(echeanceQuery);
            when(echeanceQuery.setParameter(anyString(), any())).thenReturn(echeanceQuery);
            when(echeanceQuery.getSingleResult()).thenReturn(null);

            // SUM montantPaye (totalPayeAnnee) → null (→ L868: false → BigDecimal.ZERO)
            @SuppressWarnings("unchecked")
            TypedQuery<BigDecimal> totalPayeQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("montantPaye"), eq(BigDecimal.class)))
                    .thenReturn(totalPayeQuery);
            when(totalPayeQuery.setParameter(anyString(), any())).thenReturn(totalPayeQuery);
            when(totalPayeQuery.getSingleResult()).thenReturn(null);

            Map<String, Object> synthese = cotisationService.getMesCotisationsSynthese();

            assertThat(synthese).isNotNull();
            // L865 : cotisationsEnAttente == null → 0
            assertThat(((Number) synthese.get("cotisationsEnAttente")).intValue()).isEqualTo(0);
            // L868 : totalPayeAnnee == null → BigDecimal.ZERO
            assertThat((BigDecimal) synthese.get("totalPayeAnnee"))
                    .isEqualByComparingTo(BigDecimal.ZERO);
        }

        /**
         * Couvre la ligne 866 (montantDu == null → BigDecimal.ZERO) dans la branche MEMBRE.
         */
        @Test
        @TestSecurity(user = "membre2@unionflow.com", roles = {"MEMBRE"})
        @DisplayName("MEMBRE : montantDu=null → fallback BigDecimal.ZERO (L866)")
        void synthese_membre_montantDuNull_fallbackZero() {
            UUID membreId = UUID.randomUUID();
            Membre membre = new Membre();
            membre.setId(membreId);
            membre.setNumeroMembre("UF-TEST-MONTANT-NULL");
            membre.setEmail("membre2@unionflow.com");
            membre.setActif(true);

            when(securiteHelper.resolveEmail()).thenReturn("membre2@unionflow.com");
            when(securiteHelper.getRoles()).thenReturn(Set.of("MEMBRE"));
            when(membreRepository.findByEmail("membre2@unionflow.com"))
                    .thenReturn(Optional.of(membre));

            EntityManager mockEm = mock(EntityManager.class);
            when(cotisationRepository.getEntityManager()).thenReturn(mockEm);

            // COUNT retourne une valeur non null
            @SuppressWarnings("unchecked")
            TypedQuery<Long> countQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("COUNT"), eq(Long.class))).thenReturn(countQuery);
            when(countQuery.setParameter(anyString(), any())).thenReturn(countQuery);
            when(countQuery.getSingleResult()).thenReturn(2L);

            // MIN dateEcheance → null
            @SuppressWarnings("unchecked")
            TypedQuery<LocalDate> echeanceQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("MIN"), eq(LocalDate.class))).thenReturn(echeanceQuery);
            when(echeanceQuery.setParameter(anyString(), any())).thenReturn(echeanceQuery);
            when(echeanceQuery.getSingleResult()).thenReturn(null);

            // totalPayeAnnee retourne une valeur valide
            // NOTE: enregistré AVANT le stub montantDu car la requête montantDu contient aussi
            // "montantPaye" — Mockito retourne le dernier stub enregistré qui correspond,
            // donc montantDu doit être enregistré en dernier pour prendre la priorité.
            @SuppressWarnings("unchecked")
            TypedQuery<BigDecimal> totalPayeQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("EXTRACT(YEAR FROM"), eq(BigDecimal.class)))
                    .thenReturn(totalPayeQuery);
            when(totalPayeQuery.setParameter(anyString(), any())).thenReturn(totalPayeQuery);
            when(totalPayeQuery.getSingleResult()).thenReturn(BigDecimal.valueOf(1000));

            // montantDu → null (→ L866: false → BigDecimal.ZERO)
            // Enregistré EN DERNIER : la requête montantDu contient "montantDu" ET "montantPaye",
            // donc ce stub (plus récent) prend la priorité sur le stub totalPaye pour cette requête.
            @SuppressWarnings("unchecked")
            TypedQuery<BigDecimal> montantDuQuery = mock(TypedQuery.class);
            when(mockEm.createQuery(contains("montantDu"), eq(BigDecimal.class)))
                    .thenReturn(montantDuQuery);
            when(montantDuQuery.setParameter(anyString(), any())).thenReturn(montantDuQuery);
            when(montantDuQuery.getSingleResult()).thenReturn(null);

            Map<String, Object> synthese = cotisationService.getMesCotisationsSynthese();

            assertThat(synthese).isNotNull();
            // L866 : montantDu == null → BigDecimal.ZERO
            assertThat((BigDecimal) synthese.get("montantDu")).isEqualByComparingTo(BigDecimal.ZERO);
            // cotisationsEnAttente valide → 2
            assertThat(((Number) synthese.get("cotisationsEnAttente")).intValue()).isEqualTo(2);
            // totalPayeAnnee valide → 1000
            assertThat((BigDecimal) synthese.get("totalPayeAnnee"))
                    .isEqualByComparingTo(BigDecimal.valueOf(1000));
        }
    }

    // =========================================================================
    // Branches getModePaiementIcon / getModePaiementLibelle (private static)
    // Ces méthodes ne sont appelées qu'avec null en prod → switch cases jamais atteints.
    // On les couvre via réflexion.
    // =========================================================================

    @Nested
    @DisplayName("getModePaiementIcon — branches switch")
    class GetModePaiementIconTests {

        private String invokeIcon(String methode) throws Exception {
            Method m = CotisationService.class.getDeclaredMethod("getModePaiementIcon", String.class);
            m.setAccessible(true);
            return (String) m.invoke(null, methode);
        }

        @Test @DisplayName("WAVE_MONEY → pi-mobile")
        void icon_WAVE_MONEY() throws Exception {
            assertThat(invokeIcon("WAVE_MONEY")).isEqualTo("pi-mobile");
        }

        @Test @DisplayName("MOBILE_MONEY → pi-mobile")
        void icon_MOBILE_MONEY() throws Exception {
            assertThat(invokeIcon("MOBILE_MONEY")).isEqualTo("pi-mobile");
        }

        @Test @DisplayName("VIREMENT → pi-arrow-right-arrow-left")
        void icon_VIREMENT() throws Exception {
            assertThat(invokeIcon("VIREMENT")).isEqualTo("pi-arrow-right-arrow-left");
        }

        @Test @DisplayName("ESPECES → pi-money-bill")
        void icon_ESPECES() throws Exception {
            assertThat(invokeIcon("ESPECES")).isEqualTo("pi-money-bill");
        }

        @Test @DisplayName("CARTE → pi-credit-card")
        void icon_CARTE() throws Exception {
            assertThat(invokeIcon("CARTE")).isEqualTo("pi-credit-card");
        }

        @Test @DisplayName("default → pi-wallet")
        void icon_default() throws Exception {
            assertThat(invokeIcon("INCONNU")).isEqualTo("pi-wallet");
        }
    }

    @Nested
    @DisplayName("getModePaiementLibelle — branches switch")
    class GetModePaiementLibelleTests {

        private String invokeLibelle(String methode) throws Exception {
            Method m = CotisationService.class.getDeclaredMethod("getModePaiementLibelle", String.class);
            m.setAccessible(true);
            return (String) m.invoke(null, methode);
        }

        @Test @DisplayName("WAVE_MONEY → Wave Money")
        void libelle_WAVE_MONEY() throws Exception {
            assertThat(invokeLibelle("WAVE_MONEY")).isEqualTo("Wave Money");
        }

        @Test @DisplayName("MOBILE_MONEY → Mobile Money")
        void libelle_MOBILE_MONEY() throws Exception {
            assertThat(invokeLibelle("MOBILE_MONEY")).isEqualTo("Mobile Money");
        }

        @Test @DisplayName("VIREMENT → Virement")
        void libelle_VIREMENT() throws Exception {
            assertThat(invokeLibelle("VIREMENT")).isEqualTo("Virement");
        }

        @Test @DisplayName("ESPECES → Espèces")
        void libelle_ESPECES() throws Exception {
            assertThat(invokeLibelle("ESPECES")).isEqualTo("Espèces");
        }

        @Test @DisplayName("CARTE → Carte")
        void libelle_CARTE() throws Exception {
            assertThat(invokeLibelle("CARTE")).isEqualTo("Carte");
        }

        @Test @DisplayName("default → retourne la valeur brute")
        void libelle_default() throws Exception {
            assertThat(invokeLibelle("INCONNU")).isEqualTo("INCONNU");
        }
    }
}
