package dev.lions.unionflow.server.service;

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

import dev.lions.unionflow.server.api.dto.paiement.request.CreatePaiementRequest;
import dev.lions.unionflow.server.api.dto.paiement.request.DeclarerPaiementManuelRequest;
import dev.lions.unionflow.server.api.dto.paiement.request.InitierDepotEpargneRequest;
import dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest;
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse;
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse;
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.IntentionPaiement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.Paiement;
import dev.lions.unionflow.server.entity.TransactionWave;
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
import dev.lions.unionflow.server.repository.IntentionPaiementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.PaiementRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutException;
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse;
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 jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * Tests unitaires pour {@link PaiementService} — approche @InjectMock.
 *
 * Couverture : creerPaiement, validerPaiement, annulerPaiement, trouverParId,
 * trouverParNumeroReference, listerParMembre, calculerMontantTotalValides,
 * getMonHistoriquePaiements, initierPaiementEnLigne (WAVE + non-WAVE + erreurs),
 * initierDepotEpargneEnLigne, declarerPaiementManuel, toE164 branches.
 */
@QuarkusTest
class PaiementServiceTest {

    @Inject
    PaiementService paiementService;

    @InjectMock
    PaiementRepository paiementRepository;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    KeycloakService keycloakService;

    @InjectMock
    TypeReferenceRepository typeReferenceRepository;

    @InjectMock
    IntentionPaiementRepository intentionPaiementRepository;

    @InjectMock
    WaveCheckoutService waveCheckoutService;

    @InjectMock
    CompteEpargneRepository compteEpargneRepository;

    private static final String TEST_USER_EMAIL = "membre-paiement-test@unionflow.dev";

    private Membre testMembre;
    private Membre autreMembre;
    private Organisation testOrganisation;
    private Cotisation testCotisation;
    private Cotisation autreCotisation;
    private Paiement testPaiement;
    private CompteEpargne testCompte;
    private EntityManager mockEm;

    @SuppressWarnings("unchecked")
    @BeforeEach
    void setup() {
        // --- Entités en mémoire (pas de DB) ---
        testOrganisation = new Organisation();
        testOrganisation.setId(UUID.randomUUID());
        testOrganisation.setNom("Test Org");
        testOrganisation.setActif(true);

        testMembre = new Membre();
        testMembre.setId(UUID.randomUUID());
        testMembre.setEmail(TEST_USER_EMAIL);
        testMembre.setPrenom("Robert");
        testMembre.setNom("Payeur");
        testMembre.setNumeroMembre("M-001");
        testMembre.setActif(true);

        autreMembre = new Membre();
        autreMembre.setId(UUID.randomUUID());
        autreMembre.setEmail("autre@test.com");
        autreMembre.setPrenom("Autre");
        autreMembre.setNom("Membre");
        autreMembre.setNumeroMembre("M-002");
        autreMembre.setActif(true);

        testCotisation = new Cotisation();
        testCotisation.setId(UUID.randomUUID());
        testCotisation.setMembre(testMembre);
        testCotisation.setOrganisation(testOrganisation);
        testCotisation.setMontantDu(BigDecimal.valueOf(5000));
        testCotisation.setCodeDevise("XOF");
        testCotisation.setNumeroReference("COT-001");
        testCotisation.setStatut("EN_ATTENTE");
        testCotisation.setActif(true);

        autreCotisation = new Cotisation();
        autreCotisation.setId(UUID.randomUUID());
        autreCotisation.setMembre(autreMembre);
        autreCotisation.setOrganisation(testOrganisation);
        autreCotisation.setMontantDu(BigDecimal.valueOf(3000));
        autreCotisation.setCodeDevise("XOF");
        autreCotisation.setNumeroReference("COT-002");
        autreCotisation.setStatut("EN_ATTENTE");
        autreCotisation.setActif(true);

        testPaiement = new Paiement();
        testPaiement.setId(UUID.randomUUID());
        testPaiement.setNumeroReference("PAY-001");
        testPaiement.setMontant(BigDecimal.valueOf(250));
        testPaiement.setCodeDevise("XOF");
        testPaiement.setMethodePaiement("ESPECES");
        testPaiement.setStatutPaiement("EN_ATTENTE");
        testPaiement.setMembre(testMembre);
        testPaiement.setActif(true);

        testCompte = new CompteEpargne();
        testCompte.setId(UUID.randomUUID());
        testCompte.setMembre(testMembre);
        testCompte.setOrganisation(testOrganisation);
        testCompte.setActif(true);

        // --- Mocks par défaut ---
        when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com");
        when(typeReferenceRepository.findByDomaineAndCode(anyString(), anyString()))
                .thenReturn(Optional.empty());
        when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("http://localhost:8085");

        // membreRepository par défaut
        when(membreRepository.findByEmail(TEST_USER_EMAIL)).thenReturn(Optional.of(testMembre));
        when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty());
        when(membreRepository.findByIdOptional(testMembre.getId())).thenReturn(Optional.of(testMembre));

        // EntityManager mock (pour queries JPQL et find)
        mockEm = mock(EntityManager.class);
        when(paiementRepository.getEntityManager()).thenReturn(mockEm);
        when(mockEm.merge(any())).thenAnswer(inv -> inv.getArgument(0));

        TypedQuery<Paiement> mockPaiementQuery = mock(TypedQuery.class);
        when(mockEm.createQuery(anyString(), eq(Paiement.class))).thenReturn(mockPaiementQuery);
        when(mockPaiementQuery.setParameter(anyString(), any())).thenReturn(mockPaiementQuery);
        when(mockPaiementQuery.setMaxResults(anyInt())).thenReturn(mockPaiementQuery);
        when(mockPaiementQuery.getResultList()).thenReturn(List.of(testPaiement));

        TypedQuery<Cotisation> mockCotisationQuery = mock(TypedQuery.class);
        when(mockEm.createQuery(anyString(), eq(Cotisation.class))).thenReturn(mockCotisationQuery);
        when(mockCotisationQuery.setParameter(anyString(), any())).thenReturn(mockCotisationQuery);
        when(mockCotisationQuery.getResultList()).thenReturn(List.of(testCotisation));

        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);
        when(mockEm.find(eq(Cotisation.class), eq(autreCotisation.getId()))).thenReturn(autreCotisation);
        when(mockEm.find(eq(Cotisation.class), any())).thenReturn(null);

        // persist mocks : set ID après persist
        doAnswer(inv -> {
            Object arg = inv.getArgument(0);
            if (arg instanceof Paiement p && p.getId() == null) {
                p.setId(UUID.randomUUID());
            }
            return null;
        }).when(paiementRepository).persist(any(Paiement.class));

        doAnswer(inv -> {
            Object arg = inv.getArgument(0);
            if (arg instanceof IntentionPaiement ip && ip.getId() == null) {
                ip.setId(UUID.randomUUID());
            }
            return null;
        }).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));

        // compteEpargne par défaut
        when(compteEpargneRepository.findByIdOptional(testCompte.getId()))
                .thenReturn(Optional.of(testCompte));

        // paiementRepository : findPaiementById par défaut
        when(paiementRepository.findPaiementById(testPaiement.getId()))
                .thenReturn(Optional.of(testPaiement));
        when(paiementRepository.findPaiementById(any()))
                .thenReturn(Optional.empty());
        when(paiementRepository.findPaiementById(testPaiement.getId()))
                .thenReturn(Optional.of(testPaiement));

        when(paiementRepository.findByNumeroReference("PAY-001")).thenReturn(Optional.of(testPaiement));
        when(paiementRepository.findByNumeroReference(anyString())).thenReturn(Optional.empty());
        when(paiementRepository.findByNumeroReference("PAY-001")).thenReturn(Optional.of(testPaiement));

        when(paiementRepository.findByMembreId(testMembre.getId())).thenReturn(List.of(testPaiement));
        when(paiementRepository.findByMembreId(any())).thenReturn(List.of());
        when(paiementRepository.findByMembreId(testMembre.getId())).thenReturn(List.of(testPaiement));

        when(paiementRepository.calculerMontantTotalValides(any(), any()))
                .thenReturn(BigDecimal.valueOf(1000));
    }

    // =========================================================================
    // creerPaiement
    // =========================================================================

    @Test
    @DisplayName("creerPaiement avec membreId valide → crée le paiement avec le membre")
    void creerPaiement_avecMembreId_creePaiement() {
        String ref = "PAY-" + UUID.randomUUID().toString().substring(0, 8);
        CreatePaiementRequest request = CreatePaiementRequest.builder()
                .numeroReference(ref)
                .montant(new BigDecimal("250.00"))
                .codeDevise("XOF")
                .methodePaiement("ESPECES")
                .membreId(testMembre.getId())
                .build();

        PaiementResponse response = paiementService.creerPaiement(request);

        assertThat(response).isNotNull();
        assertThat(response.getId()).isNotNull();
        assertThat(response.getNumeroReference()).isEqualTo(ref);
        assertThat(response.getStatutPaiement()).isEqualTo("EN_ATTENTE");
        assertThat(response.getMontant()).isEqualByComparingTo(new BigDecimal("250.00"));
        assertThat(response.getMembreId()).isEqualTo(testMembre.getId());
    }

    @Test
    @DisplayName("creerPaiement sans membreId → crée le paiement sans membre")
    void creerPaiement_sansMembreId_creePaiementSansMembre() {
        String ref = "PAY-NOMEMBRE-" + UUID.randomUUID().toString().substring(0, 6);
        CreatePaiementRequest request = CreatePaiementRequest.builder()
                .numeroReference(ref)
                .montant(new BigDecimal("100.00"))
                .codeDevise("EUR")
                .methodePaiement("VIREMENT")
                .membreId(null)
                .build();

        PaiementResponse response = paiementService.creerPaiement(request);

        assertThat(response).isNotNull();
        assertThat(response.getMembreId()).isNull();
    }

    @Test
    @DisplayName("creerPaiement avec membreId introuvable → NotFoundException")
    void creerPaiement_membreInexistant_throwsNotFoundException() {
        UUID unknownId = UUID.randomUUID();
        when(membreRepository.findByIdOptional(unknownId)).thenReturn(Optional.empty());

        CreatePaiementRequest request = CreatePaiementRequest.builder()
                .numeroReference("PAY-UNKNOWN")
                .montant(BigDecimal.TEN)
                .codeDevise("XOF")
                .methodePaiement("ESPECES")
                .membreId(unknownId)
                .build();

        assertThatThrownBy(() -> paiementService.creerPaiement(request))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    @Test
    @DisplayName("creerPaiement avec transactionWave non null → répond avec transactionWaveId")
    void creerPaiement_avecTransactionWave_repond() {
        // Couvre la branche "if (paiement.getTransactionWave() != null)" dans convertToResponse
        // En mode mock, transactionWave reste null → on couvre la branche null ici
        CreatePaiementRequest request = CreatePaiementRequest.builder()
                .numeroReference("PAY-TW-001")
                .montant(BigDecimal.ONE)
                .codeDevise("XOF")
                .methodePaiement("ESPECES")
                .membreId(null)
                .build();

        PaiementResponse response = paiementService.creerPaiement(request);

        assertThat(response.getTransactionWaveId()).isNull(); // branche null de transactionWave
    }

    // =========================================================================
    // validerPaiement
    // =========================================================================

    @Test
    @DisplayName("validerPaiement sur paiement EN_ATTENTE → statut VALIDE avec dateValidation")
    void validerPaiement_enAttente_retourneValide() {
        Paiement paiement = new Paiement();
        paiement.setId(UUID.randomUUID());
        paiement.setNumeroReference("PAY-VAL-001");
        paiement.setMontant(BigDecimal.TEN);
        paiement.setCodeDevise("EUR");
        paiement.setMethodePaiement("VIREMENT");
        paiement.setStatutPaiement("EN_ATTENTE");
        paiement.setMembre(testMembre);
        when(paiementRepository.findPaiementById(paiement.getId())).thenReturn(Optional.of(paiement));

        PaiementResponse response = paiementService.validerPaiement(paiement.getId());

        assertThat(response.getStatutPaiement()).isEqualTo("VALIDE");
        assertThat(response.getDateValidation()).isNotNull();
    }

    @Test
    @DisplayName("validerPaiement sur paiement déjà VALIDE → retourne sans modification")
    void validerPaiement_dejaValide_retourneSansModification() {
        Paiement paiement = new Paiement();
        paiement.setId(UUID.randomUUID());
        paiement.setNumeroReference("PAY-DEJA-VAL");
        paiement.setMontant(BigDecimal.ONE);
        paiement.setCodeDevise("XOF");
        paiement.setMethodePaiement("ESPECES");
        paiement.setStatutPaiement("VALIDE");
        paiement.setMembre(testMembre);
        when(paiementRepository.findPaiementById(paiement.getId())).thenReturn(Optional.of(paiement));

        PaiementResponse response = paiementService.validerPaiement(paiement.getId());

        assertThat(response.getStatutPaiement()).isEqualTo("VALIDE");
    }

    @Test
    @DisplayName("validerPaiement avec ID inconnu → NotFoundException")
    void validerPaiement_idInconnu_throwsNotFoundException() {
        UUID unknownId = UUID.randomUUID();
        when(paiementRepository.findPaiementById(unknownId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> paiementService.validerPaiement(unknownId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Paiement non trouvé");
    }

    // =========================================================================
    // annulerPaiement
    // =========================================================================

    @Test
    @DisplayName("annulerPaiement sur paiement EN_ATTENTE → statut ANNULE")
    void annulerPaiement_enAttente_retourneAnnule() {
        Paiement paiement = new Paiement();
        paiement.setId(UUID.randomUUID());
        paiement.setNumeroReference("PAY-ANN-001");
        paiement.setMontant(BigDecimal.ONE);
        paiement.setCodeDevise("USD");
        paiement.setMethodePaiement("CARTE");
        paiement.setStatutPaiement("EN_ATTENTE");
        paiement.setMembre(testMembre);
        when(paiementRepository.findPaiementById(paiement.getId())).thenReturn(Optional.of(paiement));

        PaiementResponse response = paiementService.annulerPaiement(paiement.getId());

        assertThat(response.getStatutPaiement()).isEqualTo("ANNULE");
    }

    @Test
    @DisplayName("annulerPaiement sur paiement VALIDE → IllegalStateException")
    void annulerPaiement_dejaValide_throwsIllegalState() {
        Paiement paiement = new Paiement();
        paiement.setId(UUID.randomUUID());
        paiement.setNumeroReference("PAY-ANN-VALIDE");
        paiement.setMontant(BigDecimal.TEN);
        paiement.setCodeDevise("XOF");
        paiement.setMethodePaiement("ESPECES");
        paiement.setStatutPaiement("VALIDE");
        paiement.setMembre(testMembre);
        when(paiementRepository.findPaiementById(paiement.getId())).thenReturn(Optional.of(paiement));

        assertThatThrownBy(() -> paiementService.annulerPaiement(paiement.getId()))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("ne peut plus être annulé");
    }

    @Test
    @DisplayName("annulerPaiement avec ID inconnu → NotFoundException")
    void annulerPaiement_idInconnu_throwsNotFoundException() {
        UUID unknownId = UUID.randomUUID();
        when(paiementRepository.findPaiementById(unknownId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> paiementService.annulerPaiement(unknownId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Paiement non trouvé");
    }

    // =========================================================================
    // trouverParId
    // =========================================================================

    @Test
    @DisplayName("trouverParId avec paiement existant → retourne le paiement")
    void trouverParId_trouve_retournePaiement() {
        PaiementResponse response = paiementService.trouverParId(testPaiement.getId());

        assertThat(response).isNotNull();
        assertThat(response.getId()).isEqualTo(testPaiement.getId());
    }

    @Test
    @DisplayName("trouverParId avec ID inconnu → NotFoundException")
    void trouverParId_idInconnu_throwsNotFoundException() {
        UUID unknownId = UUID.randomUUID();
        when(paiementRepository.findPaiementById(unknownId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> paiementService.trouverParId(unknownId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Paiement non trouvé");
    }

    // =========================================================================
    // trouverParNumeroReference
    // =========================================================================

    @Test
    @DisplayName("trouverParNumeroReference avec référence existante → retourne le paiement")
    void trouverParNumeroReference_refTrouvee_retournePaiement() {
        PaiementResponse response = paiementService.trouverParNumeroReference("PAY-001");

        assertThat(response).isNotNull();
        assertThat(response.getNumeroReference()).isEqualTo("PAY-001");
    }

    @Test
    @DisplayName("trouverParNumeroReference avec référence inconnue → NotFoundException")
    void trouverParNumeroReference_refInconnue_throwsNotFoundException() {
        assertThatThrownBy(() -> paiementService.trouverParNumeroReference("REF-INCONNU-99999"))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Paiement non trouvé avec la référence");
    }

    // =========================================================================
    // listerParMembre
    // =========================================================================

    @Test
    @DisplayName("listerParMembre avec paiements → retourne la liste")
    void listerParMembre_avecPaiements_retourneListe() {
        List<PaiementSummaryResponse> results = paiementService.listerParMembre(testMembre.getId());

        assertThat(results).isNotNull().isNotEmpty();
    }

    @Test
    @DisplayName("listerParMembre avec liste contenant null → convertToSummaryResponse null branch couverte")
    void listerParMembre_avecElementNull_couvreNullBranch() {
        when(paiementRepository.findByMembreId(testMembre.getId()))
                .thenReturn(Arrays.asList(null, testPaiement));

        List<PaiementSummaryResponse> results = paiementService.listerParMembre(testMembre.getId());

        assertThat(results).hasSize(2);
        assertThat(results.get(0)).isNull(); // convertToSummaryResponse(null) → null
    }

    @Test
    @DisplayName("listerParMembre sans paiements → retourne liste vide")
    void listerParMembre_sansPaiements_retourneVide() {
        List<PaiementSummaryResponse> results = paiementService.listerParMembre(UUID.randomUUID());

        assertThat(results).isNotNull().isEmpty();
    }

    // =========================================================================
    // calculerMontantTotalValides
    // =========================================================================

    @Test
    @DisplayName("calculerMontantTotalValides → retourne la somme calculée")
    void calculerMontantTotalValides_retourneSomme() {
        BigDecimal total = paiementService.calculerMontantTotalValides(
                LocalDateTime.now().minusYears(1), LocalDateTime.now().plusDays(1));

        assertThat(total).isNotNull().isEqualByComparingTo(BigDecimal.valueOf(1000));
    }

    // =========================================================================
    // getMonHistoriquePaiements
    // =========================================================================

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("getMonHistoriquePaiements → retourne les paiements VALIDE du membre connecté")
    void getMonHistoriquePaiements_membreConnecte_retournePaiements() {
        Paiement paiementValide = new Paiement();
        paiementValide.setId(UUID.randomUUID());
        paiementValide.setNumeroReference("PAY-HIST-001");
        paiementValide.setMontant(BigDecimal.valueOf(5000));
        paiementValide.setCodeDevise("XOF");
        paiementValide.setMethodePaiement("ESPECES");
        paiementValide.setStatutPaiement("VALIDE");
        paiementValide.setDatePaiement(LocalDateTime.now());
        paiementValide.setMembre(testMembre);

        TypedQuery<Paiement> paiementQuery = mock(TypedQuery.class);
        when(mockEm.createQuery(anyString(), eq(Paiement.class))).thenReturn(paiementQuery);
        when(paiementQuery.setParameter(anyString(), any())).thenReturn(paiementQuery);
        when(paiementQuery.setMaxResults(anyInt())).thenReturn(paiementQuery);
        when(paiementQuery.getResultList()).thenReturn(List.of(paiementValide));

        List<PaiementSummaryResponse> results = paiementService.getMonHistoriquePaiements(5);

        assertThat(results).isNotNull().isNotEmpty();
        assertThat(results.get(0).statutPaiement()).isEqualTo("VALIDE");
    }

    @Test
    @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"})
    @DisplayName("getMonHistoriquePaiements — membre connecté introuvable → NotFoundException")
    void getMonHistoriquePaiements_membreNonTrouve_throwsNotFoundException() {
        when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty());

        assertThatThrownBy(() -> paiementService.getMonHistoriquePaiements(5))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    // =========================================================================
    // initierPaiementEnLigne — Wave happy path
    // =========================================================================

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne WAVE → crée session Wave et retourne wave_launch_url")
    void initierPaiementEnLigne_wave_success() throws WaveCheckoutException {
        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("wave-session-abc123", "https://wave.com/launch/abc123");
        when(waveCheckoutService.createSession(anyString(), anyString(), anyString(),
                anyString(), anyString(), anyString())).thenReturn(session);
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("WAVE")
                .numeroTelephone("771234567")
                .build();

        PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request);

        assertThat(response).isNotNull();
        assertThat(response.getTransactionId()).isNotNull();
        assertThat(response.getRedirectUrl()).isEqualTo("https://wave.com/launch/abc123");
        assertThat(response.getStatut()).isEqualTo("EN_ATTENTE");
        assertThat(response.getMethodePaiement()).isEqualTo("WAVE");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne WAVE — cotisation sans codeDevise → default XOF (couvre L320)")
    void initierPaiementEnLigne_wave_cotisationSansCodeDevise_defaultXof() throws WaveCheckoutException {
        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("wave-s-nodevise", "https://wave.com/nodevise");
        when(waveCheckoutService.createSession(anyString(), anyString(), anyString(),
                anyString(), anyString(), anyString())).thenReturn(session);
        Cotisation cotisationSansDevise = new Cotisation();
        cotisationSansDevise.setId(UUID.randomUUID());
        cotisationSansDevise.setMembre(testMembre);
        cotisationSansDevise.setOrganisation(testOrganisation);
        cotisationSansDevise.setMontantDu(BigDecimal.valueOf(3000));
        cotisationSansDevise.setCodeDevise(null); // branche null → "XOF"
        cotisationSansDevise.setNumeroReference("COT-NODEVISE");
        cotisationSansDevise.setStatut("EN_ATTENTE");
        cotisationSansDevise.setActif(true);
        when(mockEm.find(eq(Cotisation.class), eq(cotisationSansDevise.getId()))).thenReturn(cotisationSansDevise);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(cotisationSansDevise.getId())
                .methodePaiement("WAVE")
                .numeroTelephone("771234567")
                .build();

        PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request);

        assertThat(response).isNotNull();
        assertThat(response.getStatut()).isEqualTo("EN_ATTENTE");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne WAVE — numéro commençant par 0 (branche toE164)")
    void initierPaiementEnLigne_wave_numeroCommencantPar0() throws WaveCheckoutException {
        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("wave-s-0", "https://wave.com/0");
        when(waveCheckoutService.createSession(anyString(), anyString(), anyString(),
                anyString(), anyString(), anyString())).thenReturn(session);
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("WAVE")
                .numeroTelephone("077123456")  // 9 chiffres, commence par 0
                .build();

        PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request);

        assertThat(response).isNotNull();
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne WAVE — numéro avec indicatif 225 (branche toE164)")
    void initierPaiementEnLigne_wave_numeroAvecIndicatif225() throws WaveCheckoutException {
        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("wave-s-225", "https://wave.com/225");
        when(waveCheckoutService.createSession(anyString(), anyString(), anyString(),
                anyString(), anyString(), anyString())).thenReturn(session);
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("WAVE")
                .numeroTelephone("225771234567")  // 12 chiffres, commence par 225
                .build();

        PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request);

        assertThat(response).isNotNull();
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne WAVE — erreur Wave → BadRequestException")
    void initierPaiementEnLigne_wave_erreur_throwsBadRequest() throws WaveCheckoutException {
        doThrow(new WaveCheckoutException("API error"))
                .when(waveCheckoutService).createSession(anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString());
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("WAVE")
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request))
                .isInstanceOf(jakarta.ws.rs.BadRequestException.class);
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne ORANGE_MONEY → retourne URL orange-money")
    void initierPaiementEnLigne_orangeMoney_success() {
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("ORANGE_MONEY")
                .numeroTelephone("771234567")
                .build();

        PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request);

        assertThat(response).isNotNull();
        assertThat(response.getRedirectUrl()).contains("orange-money.com");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne FREE_MONEY → retourne URL free-money")
    void initierPaiementEnLigne_freeMoney_success() {
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("FREE_MONEY")
                .numeroTelephone("771234567")
                .build();

        PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request);

        assertThat(response).isNotNull();
        assertThat(response.getRedirectUrl()).contains("free-money.com");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne CARTE_BANCAIRE → retourne URL payment-gateway")
    void initierPaiementEnLigne_carteBancaire_success() {
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("CARTE_BANCAIRE")
                .numeroTelephone("771234567")
                .build();

        PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request);

        assertThat(response).isNotNull();
        assertThat(response.getRedirectUrl()).contains("payment-gateway.com");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne méthode inconnue → IllegalArgumentException")
    void initierPaiementEnLigne_methodeInconnue_throwsIllegalArgument() {
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("CHEQUE")
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("non supportée");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne — cotisation introuvable → NotFoundException")
    void initierPaiementEnLigne_cotisationInexistante_throwsNotFoundException() {
        UUID unknownId = UUID.randomUUID();
        when(mockEm.find(eq(Cotisation.class), eq(unknownId))).thenReturn(null);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(unknownId)
                .methodePaiement("WAVE")
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Cotisation non trouvée");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne — cotisation appartient à un autre membre → IllegalArgumentException")
    void initierPaiementEnLigne_cotisationAutreMembre_throwsIllegalArgument() {
        when(mockEm.find(eq(Cotisation.class), eq(autreCotisation.getId()))).thenReturn(autreCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(autreCotisation.getId())
                .methodePaiement("WAVE")
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("n'appartient pas au membre connecté");
    }

    @Test
    @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"})
    @DisplayName("initierPaiementEnLigne — membre connecté introuvable → NotFoundException")
    void initierPaiementEnLigne_membreNonTrouve_throwsNotFoundException() {
        when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty());

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("WAVE")
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    // =========================================================================
    // initierDepotEpargneEnLigne
    // =========================================================================

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierDepotEpargneEnLigne WAVE → crée intention et retourne wave_launch_url")
    void initierDepotEpargneEnLigne_wave_success() throws WaveCheckoutException {
        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("epargne-session-xyz", "https://wave.com/epargne/xyz");
        when(waveCheckoutService.createSession(anyString(), anyString(), anyString(),
                anyString(), anyString(), anyString())).thenReturn(session);

        InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder()
                .compteId(testCompte.getId())
                .montant(new BigDecimal("10000"))
                .numeroTelephone("771234567")
                .build();

        PaiementGatewayResponse response = paiementService.initierDepotEpargneEnLigne(request);

        assertThat(response).isNotNull();
        assertThat(response.getRedirectUrl()).isEqualTo("https://wave.com/epargne/xyz");
        assertThat(response.getStatut()).isEqualTo("EN_ATTENTE");
        assertThat(response.getMethodePaiement()).isEqualTo("WAVE");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierDepotEpargneEnLigne — erreur Wave → BadRequestException + intention ECHOUEE")
    void initierDepotEpargneEnLigne_wave_erreur_throwsBadRequest() throws WaveCheckoutException {
        doThrow(new WaveCheckoutException("Wave down"))
                .when(waveCheckoutService).createSession(anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString());

        InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder()
                .compteId(testCompte.getId())
                .montant(new BigDecimal("5000"))
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierDepotEpargneEnLigne(request))
                .isInstanceOf(jakarta.ws.rs.BadRequestException.class);
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierDepotEpargneEnLigne — compte introuvable → NotFoundException")
    void initierDepotEpargneEnLigne_compteInexistant_throwsNotFoundException() {
        UUID unknownId = UUID.randomUUID();
        when(compteEpargneRepository.findByIdOptional(unknownId)).thenReturn(Optional.empty());

        InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder()
                .compteId(unknownId)
                .montant(new BigDecimal("5000"))
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierDepotEpargneEnLigne(request))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Compte épargne non trouvé");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierDepotEpargneEnLigne — compte appartient à un autre membre → IllegalArgumentException")
    void initierDepotEpargneEnLigne_compteAutreMembre_throwsIllegalArgument() {
        CompteEpargne autreCompte = new CompteEpargne();
        autreCompte.setId(UUID.randomUUID());
        autreCompte.setMembre(autreMembre);
        autreCompte.setOrganisation(testOrganisation);
        when(compteEpargneRepository.findByIdOptional(autreCompte.getId()))
                .thenReturn(Optional.of(autreCompte));

        InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder()
                .compteId(autreCompte.getId())
                .montant(new BigDecimal("5000"))
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierDepotEpargneEnLigne(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("n'appartient pas au membre connecté");
    }

    @Test
    @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"})
    @DisplayName("initierDepotEpargneEnLigne — membre connecté introuvable → NotFoundException")
    void initierDepotEpargneEnLigne_membreNonTrouve_throwsNotFoundException() {
        when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty());

        InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder()
                .compteId(testCompte.getId())
                .montant(new BigDecimal("5000"))
                .numeroTelephone("771234567")
                .build();

        assertThatThrownBy(() -> paiementService.initierDepotEpargneEnLigne(request))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    // =========================================================================
    // declarerPaiementManuel
    // =========================================================================

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("declarerPaiementManuel → crée paiement avec statut EN_ATTENTE_VALIDATION")
    void declarerPaiementManuel_success() {
        TypedQuery<Cotisation> cotisationQuery = mock(TypedQuery.class);
        when(mockEm.createQuery(anyString(), eq(Cotisation.class))).thenReturn(cotisationQuery);
        when(cotisationQuery.setParameter(anyString(), any())).thenReturn(cotisationQuery);
        when(cotisationQuery.getResultList()).thenReturn(List.of(testCotisation));

        DeclarerPaiementManuelRequest request = DeclarerPaiementManuelRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("ESPECES")
                .reference("REF-MANUEL-001")
                .commentaire("Paiement au trésorier")
                .build();

        PaiementResponse response = paiementService.declarerPaiementManuel(request);

        assertThat(response).isNotNull();
        assertThat(response.getId()).isNotNull();
        assertThat(response.getStatutPaiement()).isEqualTo("EN_ATTENTE_VALIDATION");
        assertThat(response.getMethodePaiement()).isEqualTo("ESPECES");
        assertThat(response.getReferenceExterne()).isEqualTo("REF-MANUEL-001");
        assertThat(response.getCommentaire()).isEqualTo("Paiement au trésorier");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("declarerPaiementManuel — cotisation introuvable → NotFoundException")
    void declarerPaiementManuel_cotisationInexistante_throwsNotFoundException() {
        TypedQuery<Cotisation> cotisationQuery = mock(TypedQuery.class);
        when(mockEm.createQuery(anyString(), eq(Cotisation.class))).thenReturn(cotisationQuery);
        when(cotisationQuery.setParameter(anyString(), any())).thenReturn(cotisationQuery);
        when(cotisationQuery.getResultList()).thenReturn(List.of());

        DeclarerPaiementManuelRequest request = DeclarerPaiementManuelRequest.builder()
                .cotisationId(UUID.randomUUID())
                .methodePaiement("ESPECES")
                .reference("REF-001")
                .commentaire("Test")
                .build();

        assertThatThrownBy(() -> paiementService.declarerPaiementManuel(request))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Cotisation non trouvée");
    }

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("declarerPaiementManuel — cotisation appartient à un autre membre → IllegalArgumentException")
    void declarerPaiementManuel_cotisationAutreMembre_throwsIllegalArgument() {
        TypedQuery<Cotisation> cotisationQuery = mock(TypedQuery.class);
        when(mockEm.createQuery(anyString(), eq(Cotisation.class))).thenReturn(cotisationQuery);
        when(cotisationQuery.setParameter(anyString(), any())).thenReturn(cotisationQuery);
        when(cotisationQuery.getResultList()).thenReturn(List.of(autreCotisation));

        DeclarerPaiementManuelRequest request = DeclarerPaiementManuelRequest.builder()
                .cotisationId(autreCotisation.getId())
                .methodePaiement("ESPECES")
                .reference("REF-001")
                .commentaire("Test")
                .build();

        assertThatThrownBy(() -> paiementService.declarerPaiementManuel(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("n'appartient pas au membre connecté");
    }

    @Test
    @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"})
    @DisplayName("declarerPaiementManuel — membre connecté introuvable → NotFoundException")
    void declarerPaiementManuel_membreNonTrouve_throwsNotFoundException() {
        when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty());

        DeclarerPaiementManuelRequest request = DeclarerPaiementManuelRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("ESPECES")
                .reference("REF-001")
                .commentaire("Test")
                .build();

        assertThatThrownBy(() -> paiementService.declarerPaiementManuel(request))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    // =========================================================================
    // toE164 — branches manquantes via réflexion
    // =========================================================================

    @Test
    @DisplayName("toE164 null → retourne null")
    void toE164_null_returnsNull() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        Object result = toE164.invoke(null, (Object) null);
        assertThat(result).isNull();
    }

    @Test
    @DisplayName("toE164 blank string → retourne null")
    void toE164_blank_returnsNull() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        Object result = toE164.invoke(null, "   ");
        assertThat(result).isNull();
    }

    @Test
    @DisplayName("toE164 numéro 9 chiffres commençant par 7 → +225 + digits")
    void toE164_9DigitsStartingWith7_returnsWith225() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        Object result = toE164.invoke(null, "771234567");
        assertThat(result).isEqualTo("+225771234567");
    }

    @Test
    @DisplayName("toE164 numéro 9 chiffres commençant par 0 → +225 + digits[1:]")
    void toE164_9DigitsStartingWith0_removesLeadingZero() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        Object result = toE164.invoke(null, "077123456");
        assertThat(result).isEqualTo("+22577123456");
    }

    @Test
    @DisplayName("toE164 numéro commençant par 225 (>=9 chiffres) → +digits")
    void toE164_startsWithIndicatif225_returnsPlusDigits() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        Object result = toE164.invoke(null, "225771234567");
        assertThat(result).isEqualTo("+225771234567");
    }

    @Test
    @DisplayName("toE164 numéro déjà avec + → retourné tel quel")
    void toE164_alreadyHasPlus_returnsAsIs() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        Object result = toE164.invoke(null, "+33612345678");
        assertThat(result).isEqualTo("+33612345678");
    }

    @Test
    @DisplayName("toE164 numéro autre format (pas 9 chiffres, pas 225, pas +) → +digits")
    void toE164_otherFormat_returnsPlusDigits() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        Object result = toE164.invoke(null, "3361234567");
        assertThat(result).isEqualTo("+3361234567");
    }

    // =========================================================================
    // convertToResponse — branche transactionWave non-null
    // =========================================================================

    @Test
    @DisplayName("convertToResponse avec transactionWave non-null → set transactionWaveId")
    void convertToResponse_avecTransactionWave_setsTransactionWaveId() {
        UUID paiementId = UUID.randomUUID();
        dev.lions.unionflow.server.entity.TransactionWave wave =
                new dev.lions.unionflow.server.entity.TransactionWave();
        wave.setId(UUID.randomUUID());

        Paiement paiement = new Paiement();
        paiement.setId(paiementId);
        paiement.setNumeroReference("PAY-TW-BRANCH");
        paiement.setMontant(BigDecimal.ONE);
        paiement.setCodeDevise("XOF");
        paiement.setMethodePaiement("WAVE");
        paiement.setStatutPaiement("VALIDE");
        paiement.setTransactionWave(wave);  // non-null → branche transactionWave != null
        paiement.setActif(true);

        when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement));

        dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse response =
                paiementService.trouverParId(paiementId);

        assertThat(response).isNotNull();
        assertThat(response.getTransactionWaveId()).isEqualTo(wave.getId());
    }

    // =========================================================================
    // resolveLibelle — branche found (TypeReference trouvé)
    // resolveSeverity — branche found (TypeReference trouvé)
    // =========================================================================

    @Test
    @DisplayName("resolveLibelle retourne le libellé de la TypeReference quand trouvée")
    void resolveLibelle_found_returnsLibelle() {
        UUID paiementId = UUID.randomUUID();

        dev.lions.unionflow.server.entity.TypeReference typeRef =
                new dev.lions.unionflow.server.entity.TypeReference();
        typeRef.setLibelle("Espèces");
        typeRef.setSeverity("info");
        when(typeReferenceRepository.findByDomaineAndCode("METHODE_PAIEMENT", "ESPECES"))
                .thenReturn(Optional.of(typeRef));

        dev.lions.unionflow.server.entity.TypeReference statutRef =
                new dev.lions.unionflow.server.entity.TypeReference();
        statutRef.setLibelle("Validé");
        statutRef.setSeverity("success");
        when(typeReferenceRepository.findByDomaineAndCode("STATUT_PAIEMENT", "VALIDE"))
                .thenReturn(Optional.of(statutRef));

        Paiement paiement = new Paiement();
        paiement.setId(paiementId);
        paiement.setNumeroReference("PAY-LIBELLE");
        paiement.setMontant(BigDecimal.TEN);
        paiement.setCodeDevise("XOF");
        paiement.setMethodePaiement("ESPECES");
        paiement.setStatutPaiement("VALIDE");
        paiement.setActif(true);

        when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement));

        dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse response =
                paiementService.trouverParId(paiementId);

        assertThat(response).isNotNull();
        assertThat(response.getMethodePaiementLibelle()).isEqualTo("Espèces");
        assertThat(response.getStatutPaiementLibelle()).isEqualTo("Validé");
        assertThat(response.getStatutPaiementSeverity()).isEqualTo("success");
    }

    // =========================================================================
    // initierPaiementWave — branche getRedirectBaseUrl avec trailing slash
    // =========================================================================

    @Test
    @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
    @DisplayName("initierPaiementWave avec URL de base ayant un slash final → slash supprimé")
    void initierPaiementWave_urlWithTrailingSlash_removesSlash() throws Exception {
        // Configurer la base URL avec un slash final → replaceAll("/+$", "") sera exercé
        when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("http://localhost:8085/");

        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse(
                "session-slash-test", "https://wave.com/slash");
        when(waveCheckoutService.createSession(anyString(), anyString(), anyString(),
                anyString(), anyString(), anyString())).thenReturn(session);
        when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation);

        InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("WAVE")
                .numeroTelephone("771234567")
                .build();

        dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse response =
                paiementService.initierPaiementEnLigne(request);

        assertThat(response).isNotNull();
        assertThat(response.getRedirectUrl()).isEqualTo("https://wave.com/slash");
    }

    // =========================================================================
    // resolveSeverity — branche code null (retourne null immédiatement)
    // =========================================================================

    @Test
    @DisplayName("convertToResponse avec membre=null — branche if(membre!=null) false couverte")
    void convertToResponse_membreNull_membreIdNotSet() {
        UUID paiementId = UUID.randomUUID();

        Paiement paiement = new Paiement();
        paiement.setId(paiementId);
        paiement.setNumeroReference("PAY-NO-MEMBRE");
        paiement.setMontant(BigDecimal.TEN);
        paiement.setCodeDevise("XOF");
        paiement.setMembre(null); // branche if(membre != null) → false
        paiement.setActif(true);

        when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement));

        dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse response =
                paiementService.trouverParId(paiementId);

        assertThat(response).isNotNull();
        assertThat(response.getMembreId()).isNull();
    }

    @Test
    @DisplayName("resolveSeverity avec code null retourne null (via convertToSummaryResponse)")
    void resolveSeverity_nullCode_returnsNull() throws Exception {
        // paiement sans statutPaiement et sans methodePaiement → code null dans resolveLibelle/resolveSeverity
        Paiement paiement = new Paiement();
        paiement.setId(UUID.randomUUID());
        paiement.setNumeroReference("PAY-NULL-CODE");
        paiement.setMontant(BigDecimal.ONE);
        paiement.setCodeDevise("XOF");
        paiement.setMethodePaiement(null);   // → resolveLibelle avec code null → return null
        paiement.setStatutPaiement(null);    // → resolveSeverity avec code null → return null
        paiement.setActif(true);

        when(paiementRepository.findByMembreId(testMembre.getId()))
                .thenReturn(List.of(paiement));

        // listerParMembre appelle convertToSummaryResponse qui appelle resolveLibelle/resolveSeverity
        List<PaiementSummaryResponse> results = paiementService.listerParMembre(testMembre.getId());

        assertThat(results).isNotNull();
        assertThat(results).isNotEmpty();
    }

    @Test
    @DisplayName("toE164 numéro 9 chiffres ne commençant ni par 7 ni par 0 → +digits")
    void toE164_9DigitsNotStartingWith7Or0_returnsPlusDigits() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        // 9 chiffres commençant par 5 (ni 7 ni 0) → aucune règle de normalisation ne s'applique → préfixe +
        Object result = toE164.invoke(null, "577123456");
        assertThat(result).isEqualTo("+577123456");
    }

    @Test
    @DisplayName("toE164 numéro court (< 9 chiffres) → +digits")
    void toE164_shortNumber_length8_skipsL395() throws Exception {
        java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class);
        toE164.setAccessible(true);
        // 8 chiffres → aucune règle de normalisation ne s'applique → préfixe +
        Object result = toE164.invoke(null, "12345678");
        assertThat(result).isEqualTo("+12345678");
    }

    @Test
    @DisplayName("enrichirLibelles — methodePaiement null, statutPaiement non-null → libellé méthode absent")
    void enrichirLibelles_methodePaiementNull_statutNonNull_covers608() {
        UUID paiementId = UUID.randomUUID();

        Paiement paiement = new Paiement();
        paiement.setId(paiementId);
        paiement.setNumeroReference("PAY-608-BRANCH");
        paiement.setMontant(BigDecimal.TEN);
        paiement.setCodeDevise("XOF");
        paiement.setMethodePaiement(null);
        paiement.setStatutPaiement("EN_ATTENTE");
        paiement.setActif(true);

        when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement));

        dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse response =
                paiementService.trouverParId(paiementId);

        assertThat(response).isNotNull();
        assertThat(response.getMethodePaiementLibelle()).isNull();
    }
}
