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.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
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.versement.request.DeclarerVersementManuelRequest;
import dev.lions.unionflow.server.api.dto.versement.request.InitierDepotEpargneRequest;
import dev.lions.unionflow.server.api.dto.versement.request.InitierVersementWaveRequest;
import dev.lions.unionflow.server.api.dto.versement.response.VersementGatewayResponse;
import dev.lions.unionflow.server.api.dto.versement.response.VersementResponse;
import dev.lions.unionflow.server.api.dto.versement.response.VersementStatutResponse;
import dev.lions.unionflow.server.api.dto.versement.response.VersementSummaryResponse;
import dev.lions.unionflow.server.api.enums.paiement.StatutIntentionPaiement;
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.Versement;
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
import dev.lions.unionflow.server.repository.IntentionPaiementRepository;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import dev.lions.unionflow.server.repository.VersementRepository;
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.Collections;
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 VersementService}.
 *
 * Couverture : trouverParId, trouverParNumeroReference, listerParMembre,
 * calculerMontantTotalConfirmes, getMesVersements, validerVersement,
 * annulerVersement, initierVersementWave (succès + Wave error + cotisation
 * inconnue + cotisation d'un autre membre), declarerVersementManuel,
 * verifierStatutVersement (complété / expiré / en attente),
 * confirmerVersementWave (idempotence), toE164 (toutes branches).
 */
@QuarkusTest
@DisplayName("VersementService")
class VersementServiceTest {

    @Inject
    VersementService versementService;

    @InjectMock VersementRepository versementRepository;
    @InjectMock MembreRepository membreRepository;
    @InjectMock KeycloakService keycloakService;
    @InjectMock TypeReferenceRepository typeReferenceRepository;
    @InjectMock IntentionPaiementRepository intentionPaiementRepository;
    @InjectMock WaveCheckoutService waveCheckoutService;
    @InjectMock CompteEpargneRepository compteEpargneRepository;
    @InjectMock MembreOrganisationRepository membreOrganisationRepository;
    @InjectMock NotificationService notificationService;

    private Membre testMembre;
    private Membre autreMembre;
    private Organisation testOrg;
    private Cotisation testCotisation;
    private Versement testVersement;
    private CompteEpargne testCompte;
    private EntityManager mockEm;

    @BeforeEach
    void setUp() {
        testOrg = new Organisation();
        testOrg.setId(UUID.randomUUID());
        testOrg.setNom("Org Test");

        testMembre = new Membre();
        testMembre.setId(UUID.randomUUID());
        testMembre.setNumeroMembre("M001");
        testMembre.setEmail("membre@test.com");
        testMembre.setPrenom("Jean");
        testMembre.setNom("Dupont");
        testMembre.setDateNaissance(LocalDate.of(1990, 1, 1));

        autreMembre = new Membre();
        autreMembre.setId(UUID.randomUUID());
        autreMembre.setNumeroMembre("M002");
        autreMembre.setEmail("autre@test.com");
        autreMembre.setDateNaissance(LocalDate.of(1985, 5, 15));

        testCotisation = new Cotisation();
        testCotisation.setId(UUID.randomUUID());
        testCotisation.setNumeroReference("COT-2026-001");
        testCotisation.setMembre(testMembre);
        testCotisation.setOrganisation(testOrg);
        testCotisation.setMontantDu(new BigDecimal("5000"));
        testCotisation.setCodeDevise("XOF");
        testCotisation.setStatut("EN_ATTENTE");
        testCotisation.setDateEcheance(LocalDate.now().plusDays(30));
        testCotisation.setTypeCotisation("MENSUELLE");
        testCotisation.setLibelle("Cotisation janvier 2026");
        testCotisation.setAnnee(2026);

        testVersement = new Versement();
        testVersement.setId(UUID.randomUUID());
        testVersement.setNumeroReference("VRS-2026-001");
        testVersement.setMontant(new BigDecimal("5000"));
        testVersement.setCodeDevise("XOF");
        testVersement.setMethodePaiement("WAVE");
        testVersement.setStatutPaiement("EN_ATTENTE");
        testVersement.setMembre(testMembre);
        testVersement.setDatePaiement(LocalDateTime.now());

        testCompte = new CompteEpargne();
        testCompte.setId(UUID.randomUUID());
        testCompte.setMembre(testMembre);
        testCompte.setOrganisation(testOrg);

        mockEm = mock(EntityManager.class);
        when(versementRepository.getEntityManager()).thenReturn(mockEm);
        when(typeReferenceRepository.findByDomaineAndCode(anyString(), anyString()))
            .thenReturn(Optional.empty());
        when(keycloakService.getCurrentUserEmail()).thenReturn(testMembre.getEmail());
    }

    // ── trouverParId ──────────────────────────────────────────────────────────

    @Test
    @DisplayName("trouverParId — trouvé → retourne response")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void trouverParId_found() {
        when(versementRepository.findVersementById(testVersement.getId()))
            .thenReturn(Optional.of(testVersement));

        VersementResponse r = versementService.trouverParId(testVersement.getId());

        assertThat(r.getNumeroReference()).isEqualTo("VRS-2026-001");
    }

    @Test
    @DisplayName("trouverParId — non trouvé → NotFoundException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void trouverParId_notFound() {
        when(versementRepository.findVersementById(any())).thenReturn(Optional.empty());

        assertThatThrownBy(() -> versementService.trouverParId(UUID.randomUUID()))
            .isInstanceOf(NotFoundException.class);
    }

    // ── trouverParNumeroReference ─────────────────────────────────────────────

    @Test
    @DisplayName("trouverParNumeroReference — trouvé → response")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void trouverParNumeroReference_found() {
        when(versementRepository.findByNumeroReference("VRS-2026-001"))
            .thenReturn(Optional.of(testVersement));

        VersementResponse r = versementService.trouverParNumeroReference("VRS-2026-001");
        assertThat(r).isNotNull();
    }

    @Test
    @DisplayName("trouverParNumeroReference — non trouvé → NotFoundException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void trouverParNumeroReference_notFound() {
        when(versementRepository.findByNumeroReference(anyString())).thenReturn(Optional.empty());

        assertThatThrownBy(() -> versementService.trouverParNumeroReference("VRS-INCONNU"))
            .isInstanceOf(NotFoundException.class);
    }

    // ── listerParMembre ───────────────────────────────────────────────────────

    @Test
    @DisplayName("listerParMembre — retourne liste non nulle")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void listerParMembre_returnsList() {
        when(versementRepository.findByMembreId(testMembre.getId()))
            .thenReturn(List.of(testVersement));

        List<VersementSummaryResponse> list = versementService.listerParMembre(testMembre.getId());
        assertThat(list).hasSize(1);
    }

    // ── calculerMontantTotalConfirmes ─────────────────────────────────────────

    @Test
    @DisplayName("calculerMontantTotalConfirmes — délègue au repository")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void calculerMontantTotalConfirmes_delegates() {
        LocalDateTime debut = LocalDateTime.now().minusDays(30);
        LocalDateTime fin   = LocalDateTime.now();
        when(versementRepository.calculerMontantTotalConfirmes(debut, fin))
            .thenReturn(new BigDecimal("15000"));

        BigDecimal total = versementService.calculerMontantTotalConfirmes(debut, fin);
        assertThat(total).isEqualByComparingTo("15000");
    }

    // ── getMesVersements ──────────────────────────────────────────────────────

    @Test
    @DisplayName("getMesVersements — retourne historique du membre connecté")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void getMesVersements_returnsHistory() {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        @SuppressWarnings("unchecked")
        TypedQuery<Versement> query = mock(TypedQuery.class);
        doReturn(query).when(mockEm).createQuery(anyString(), any());
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.setMaxResults(anyInt())).thenReturn(query);
        when(query.getResultList()).thenReturn(List.of(testVersement));

        List<VersementSummaryResponse> list = versementService.getMesVersements(5);
        assertThat(list).hasSize(1);
    }

    @Test
    @DisplayName("getMesVersements — membre non trouvé → NotFoundException")
    @TestSecurity(user = "inconnu@test.com", roles = {"MEMBRE"})
    void getMesVersements_memberNotFound() {
        when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty());

        assertThatThrownBy(() -> versementService.getMesVersements(5))
            .isInstanceOf(NotFoundException.class);
    }

    // ── validerVersement ──────────────────────────────────────────────────────

    @Test
    @DisplayName("validerVersement — passe à CONFIRME")
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    void validerVersement_setsConfirme() {
        when(versementRepository.findVersementById(testVersement.getId()))
            .thenReturn(Optional.of(testVersement));

        VersementResponse r = versementService.validerVersement(testVersement.getId());
        assertThat(r.getStatutPaiement()).isEqualTo("CONFIRME");
    }

    @Test
    @DisplayName("validerVersement — déjà confirmé → idempotent")
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    void validerVersement_alreadyConfirme_idempotent() {
        testVersement.setStatutPaiement("CONFIRME");
        when(versementRepository.findVersementById(testVersement.getId()))
            .thenReturn(Optional.of(testVersement));

        VersementResponse r = versementService.validerVersement(testVersement.getId());
        assertThat(r.getStatutPaiement()).isEqualTo("CONFIRME");
    }

    @Test
    @DisplayName("validerVersement — non trouvé → NotFoundException")
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    void validerVersement_notFound() {
        when(versementRepository.findVersementById(any())).thenReturn(Optional.empty());

        assertThatThrownBy(() -> versementService.validerVersement(UUID.randomUUID()))
            .isInstanceOf(NotFoundException.class);
    }

    // ── annulerVersement ──────────────────────────────────────────────────────

    @Test
    @DisplayName("annulerVersement — passe à ANNULE")
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    void annulerVersement_setsAnnule() {
        when(versementRepository.findVersementById(testVersement.getId()))
            .thenReturn(Optional.of(testVersement));

        VersementResponse r = versementService.annulerVersement(testVersement.getId());
        assertThat(r.getStatutPaiement()).isEqualTo("ANNULE");
    }

    @Test
    @DisplayName("annulerVersement — déjà ANNULE → IllegalStateException")
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    void annulerVersement_alreadyAnnule() {
        testVersement.setStatutPaiement("ANNULE");
        when(versementRepository.findVersementById(testVersement.getId()))
            .thenReturn(Optional.of(testVersement));

        assertThatThrownBy(() -> versementService.annulerVersement(testVersement.getId()))
            .isInstanceOf(IllegalStateException.class);
    }

    // ── initierVersementWave ──────────────────────────────────────────────────

    @Test
    @DisplayName("initierVersementWave — succès Wave → retourne waveLaunchUrl")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierVersementWave_success() throws WaveCheckoutException {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        when(mockEm.find(Cotisation.class, testCotisation.getId()))
            .thenReturn(testCotisation);
        when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");

        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-abc123", "wave://checkout/abc123");
        when(waveCheckoutService.createSession(
            anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
            .thenReturn(session);
        doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
        doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class));
        when(mockEm.merge(any())).thenReturn(testCotisation);

        InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
            .cotisationId(testCotisation.getId())
            .numeroTelephone("771234567")
            .build();

        VersementGatewayResponse r = versementService.initierVersementWave(request);

        assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/abc123");
        assertThat(r.getWaveCheckoutSessionId()).isEqualTo("cos-abc123");
        assertThat(r.getMontant()).isEqualByComparingTo("5000");
        assertThat(r.getClientReference()).isNotNull();
    }

    @Test
    @DisplayName("initierVersementWave — cotisation inconnue → NotFoundException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierVersementWave_cotisationInconnue() {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        when(mockEm.find(Cotisation.class, any())).thenReturn(null);

        InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
            .cotisationId(UUID.randomUUID())
            .numeroTelephone("771234567")
            .build();

        assertThatThrownBy(() -> versementService.initierVersementWave(request))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("initierVersementWave — cotisation d'un autre membre → IllegalArgumentException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierVersementWave_cotisationAutreMembre() {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        testCotisation.setMembre(autreMembre);
        when(mockEm.find(Cotisation.class, testCotisation.getId()))
            .thenReturn(testCotisation);

        InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
            .cotisationId(testCotisation.getId())
            .numeroTelephone("771234567")
            .build();

        assertThatThrownBy(() -> versementService.initierVersementWave(request))
            .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    @DisplayName("initierVersementWave — Wave API error → BadRequestException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierVersementWave_waveApiError() throws WaveCheckoutException {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        when(mockEm.find(Cotisation.class, testCotisation.getId()))
            .thenReturn(testCotisation);
        when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");
        doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
        doThrow(new WaveCheckoutException("Wave down"))
            .when(waveCheckoutService).createSession(
                anyString(), anyString(), anyString(), anyString(), anyString(), anyString());

        InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
            .cotisationId(testCotisation.getId())
            .numeroTelephone("771234567")
            .build();

        assertThatThrownBy(() -> versementService.initierVersementWave(request))
            .isInstanceOf(jakarta.ws.rs.BadRequestException.class);
    }

    @Test
    @DisplayName("initierVersementWave — sans numéro téléphone (web QR) → successUrl web")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierVersementWave_noPhone_webContext() throws WaveCheckoutException {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        when(mockEm.find(Cotisation.class, testCotisation.getId()))
            .thenReturn(testCotisation);
        when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");

        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-web", "wave://checkout/web");
        when(waveCheckoutService.createSession(
            anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
            .thenReturn(session);
        doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
        doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class));
        when(mockEm.merge(any())).thenReturn(testCotisation);

        InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
            .cotisationId(testCotisation.getId())
            .build(); // pas de numéro → web

        VersementGatewayResponse r = versementService.initierVersementWave(request);
        assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/web");
    }

    // ── initierDepotEpargneEnLigne ────────────────────────────────────────────

    @Test
    @DisplayName("initierDepotEpargneEnLigne — succès Wave")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierDepotEpargne_success() throws WaveCheckoutException {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        when(compteEpargneRepository.findByIdOptional(testCompte.getId()))
            .thenReturn(Optional.of(testCompte));
        when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");

        WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-epargne", "wave://checkout/epargne");
        when(waveCheckoutService.createSession(
            anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
            .thenReturn(session);
        doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));

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

        VersementGatewayResponse r = versementService.initierDepotEpargneEnLigne(request);
        assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/epargne");
        assertThat(r.getMontant()).isEqualByComparingTo("10000");
    }

    @Test
    @DisplayName("initierDepotEpargneEnLigne — compte inconnu → NotFoundException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierDepotEpargne_compteInconnu() {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        when(compteEpargneRepository.findByIdOptional(any())).thenReturn(Optional.empty());

        assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne(
            InitierDepotEpargneRequest.builder()
                .compteId(UUID.randomUUID())
                .montant(BigDecimal.TEN)
                .build()))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("initierDepotEpargneEnLigne — compte d'un autre membre → IllegalArgumentException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierDepotEpargne_compteAutreMembre() {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        testCompte.setMembre(autreMembre);
        when(compteEpargneRepository.findByIdOptional(testCompte.getId()))
            .thenReturn(Optional.of(testCompte));

        assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne(
            InitierDepotEpargneRequest.builder()
                .compteId(testCompte.getId())
                .montant(BigDecimal.TEN)
                .build()))
            .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    @DisplayName("initierDepotEpargneEnLigne — Wave error → BadRequestException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void initierDepotEpargne_waveError() throws WaveCheckoutException {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        when(compteEpargneRepository.findByIdOptional(testCompte.getId()))
            .thenReturn(Optional.of(testCompte));
        when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");
        doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
        doThrow(new WaveCheckoutException("Wave down"))
            .when(waveCheckoutService).createSession(
                anyString(), anyString(), anyString(), anyString(), anyString(), anyString());

        assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne(
            InitierDepotEpargneRequest.builder()
                .compteId(testCompte.getId())
                .montant(BigDecimal.TEN)
                .build()))
            .isInstanceOf(jakarta.ws.rs.BadRequestException.class);
    }

    // ── declarerVersementManuel ───────────────────────────────────────────────

    @Test
    @DisplayName("declarerVersementManuel — créé EN_ATTENTE_VALIDATION")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void declarerVersementManuel_success() {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        @SuppressWarnings("unchecked")
        TypedQuery<Cotisation> query = mock(TypedQuery.class);
        doReturn(query).when(mockEm).createQuery(anyString(), any());
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getResultList()).thenReturn(List.of(testCotisation));
        doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class));
        when(membreOrganisationRepository.findFirstByMembreId(any()))
            .thenReturn(Optional.empty());

        DeclarerVersementManuelRequest request = DeclarerVersementManuelRequest.builder()
            .cotisationId(testCotisation.getId())
            .methodePaiement("ESPECES")
            .commentaire("Remis en main propre")
            .build();

        VersementResponse r = versementService.declarerVersementManuel(request);
        assertThat(r.getStatutPaiement()).isEqualTo("EN_ATTENTE_VALIDATION");
        assertThat(r.getMethodePaiement()).isEqualTo("ESPECES");
    }

    @Test
    @DisplayName("declarerVersementManuel — cotisation inconnue → NotFoundException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void declarerVersementManuel_cotisationInconnue() {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        @SuppressWarnings("unchecked")
        TypedQuery<Cotisation> query = mock(TypedQuery.class);
        doReturn(query).when(mockEm).createQuery(anyString(), any());
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getResultList()).thenReturn(Collections.emptyList());

        assertThatThrownBy(() -> versementService.declarerVersementManuel(
            DeclarerVersementManuelRequest.builder()
                .cotisationId(UUID.randomUUID())
                .methodePaiement("ESPECES")
                .build()))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("declarerVersementManuel — cotisation d'un autre membre → IllegalArgumentException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void declarerVersementManuel_cotisationAutreMembre() {
        when(membreRepository.findByEmail("membre@test.com"))
            .thenReturn(Optional.of(testMembre));
        testCotisation.setMembre(autreMembre);
        @SuppressWarnings("unchecked")
        TypedQuery<Cotisation> query = mock(TypedQuery.class);
        doReturn(query).when(mockEm).createQuery(anyString(), any());
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getResultList()).thenReturn(List.of(testCotisation));

        assertThatThrownBy(() -> versementService.declarerVersementManuel(
            DeclarerVersementManuelRequest.builder()
                .cotisationId(testCotisation.getId())
                .methodePaiement("ESPECES")
                .build()))
            .isInstanceOf(IllegalArgumentException.class);
    }

    // ── verifierStatutVersement ───────────────────────────────────────────────

    @Test
    @DisplayName("verifierStatutVersement — intention inconnue → NotFoundException")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void verifierStatutVersement_notFound() {
        when(intentionPaiementRepository.findById(any())).thenReturn(null);

        assertThatThrownBy(() -> versementService.verifierStatutVersement(UUID.randomUUID()))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("verifierStatutVersement — déjà COMPLETEE → confirme=true")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void verifierStatutVersement_completee() {
        IntentionPaiement intention = buildIntention(StatutIntentionPaiement.COMPLETEE);
        when(intentionPaiementRepository.findById(intention.getId()))
            .thenReturn(intention);

        VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
        assertThat(r.isConfirme()).isTrue();
        assertThat(r.getMessage()).contains("confirmé");
    }

    @Test
    @DisplayName("verifierStatutVersement — EXPIREE → confirme=false")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void verifierStatutVersement_expiree() {
        IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EXPIREE);
        when(intentionPaiementRepository.findById(intention.getId()))
            .thenReturn(intention);

        VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
        assertThat(r.isConfirme()).isFalse();
    }

    @Test
    @DisplayName("verifierStatutVersement — ECHOUEE → confirme=false")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void verifierStatutVersement_echouee() {
        IntentionPaiement intention = buildIntention(StatutIntentionPaiement.ECHOUEE);
        when(intentionPaiementRepository.findById(intention.getId()))
            .thenReturn(intention);

        VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
        assertThat(r.isConfirme()).isFalse();
    }

    @Test
    @DisplayName("verifierStatutVersement — session expirée localement → EXPIREE")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void verifierStatutVersement_localExpiry() {
        IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS);
        intention.setDateExpiration(LocalDateTime.now().minusMinutes(5));
        when(intentionPaiementRepository.findById(intention.getId()))
            .thenReturn(intention);

        VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
        assertThat(r.getStatut()).isEqualTo("EXPIREE");
    }

    @Test
    @DisplayName("verifierStatutVersement — EN_COURS sans session Wave → en attente")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void verifierStatutVersement_enCoursNoSession() {
        IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS);
        intention.setDateExpiration(LocalDateTime.now().plusMinutes(25));
        when(intentionPaiementRepository.findById(intention.getId()))
            .thenReturn(intention);

        VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
        assertThat(r.isConfirme()).isFalse();
        assertThat(r.getMessage()).containsIgnoringCase("attente");
    }

    // ── confirmerVersementWave ────────────────────────────────────────────────

    @Test
    @DisplayName("confirmerVersementWave — déjà COMPLETEE → idempotent")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void confirmerVersementWave_alreadyCompletee_idempotent() {
        IntentionPaiement intention = buildIntention(StatutIntentionPaiement.COMPLETEE);
        // Ne doit pas appeler persist
        versementService.confirmerVersementWave(intention, null);
        // Pas d'exception = idempotence OK
    }

    @Test
    @DisplayName("confirmerVersementWave — objetsCibles null → passe sans erreur")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void confirmerVersementWave_nullObjetsCibles() {
        IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS);
        intention.setObjetsCibles(null);
        doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));

        versementService.confirmerVersementWave(intention, "TCN-123");

        assertThat(intention.getStatut()).isEqualTo(StatutIntentionPaiement.COMPLETEE);
    }

    @Test
    @DisplayName("confirmerVersementWave — JSON cotisation → mise à jour cotisation")
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    void confirmerVersementWave_reconcilesCotisation() {
        UUID cotisationId = testCotisation.getId();
        IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS);
        intention.setObjetsCibles(
            "[{\"type\":\"COTISATION\",\"id\":\"" + cotisationId + "\",\"montant\":5000}]");
        doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
        when(mockEm.find(Cotisation.class, cotisationId)).thenReturn(testCotisation);
        when(mockEm.merge(any())).thenReturn(testCotisation);

        versementService.confirmerVersementWave(intention, "TCN-ABC");

        assertThat(testCotisation.getStatut()).isEqualTo("PAYEE");
        assertThat(testCotisation.getMontantPaye()).isEqualByComparingTo("5000");
    }

    // ── toE164 ────────────────────────────────────────────────────────────────

    @Test
    @DisplayName("toE164 — null → null")
    void toE164_null() {
        assertThat(VersementService.toE164(null)).isNull();
    }

    @Test
    @DisplayName("toE164 — vide → null")
    void toE164_blank() {
        assertThat(VersementService.toE164("   ")).isNull();
    }

    @Test
    @DisplayName("toE164 — 9 chiffres commençant par 7 → +221 prefix")
    void toE164_9digits_7prefix() {
        assertThat(VersementService.toE164("771234567")).isEqualTo("+221771234567");
    }

    @Test
    @DisplayName("toE164 — 9 chiffres commençant par 0 → +221 + supprime 0")
    void toE164_9digits_0prefix() {
        assertThat(VersementService.toE164("071234567")).isEqualTo("+22171234567");
    }

    @Test
    @DisplayName("toE164 — déjà avec indicatif 221 → ajoute +")
    void toE164_with221prefix() {
        assertThat(VersementService.toE164("221771234567")).isEqualTo("+221771234567");
    }

    @Test
    @DisplayName("toE164 — déjà au format E.164 (+221...) → conservé")
    void toE164_alreadyE164() {
        assertThat(VersementService.toE164("+221771234567")).isEqualTo("+221771234567");
    }

    @Test
    @DisplayName("toE164 — format non reconnu → + + chiffres")
    void toE164_unknown() {
        assertThat(VersementService.toE164("0033612345678")).startsWith("+");
    }

    // ── helpers ───────────────────────────────────────────────────────────────

    private IntentionPaiement buildIntention(StatutIntentionPaiement statut) {
        IntentionPaiement i = new IntentionPaiement();
        i.setId(UUID.randomUUID());
        i.setStatut(statut);
        i.setMontantTotal(new BigDecimal("5000"));
        i.setCodeDevise("XOF");
        i.setUtilisateur(testMembre);
        i.setDateExpiration(LocalDateTime.now().plusMinutes(30));
        return i;
    }
}
