package dev.lions.unionflow.server.service.mutuelle.epargne;

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.isNull;
import static org.mockito.Mockito.*;

import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneRequest;
import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneResponse;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.StatutCompteEpargne;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeTransactionEpargne;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
import dev.lions.unionflow.server.entity.mutuelle.epargne.TransactionEpargne;
import dev.lions.unionflow.server.mapper.mutuelle.epargne.TransactionEpargneMapper;
import dev.lions.unionflow.server.repository.ParametresLcbFtRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.TransactionEpargneRepository;
import dev.lions.unionflow.server.service.AuditService;
import dev.lions.unionflow.server.service.AlerteLcbFtService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
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;

@QuarkusTest
class TransactionEpargneServiceTest {

    @Inject
    TransactionEpargneService service;

    @InjectMock
    TransactionEpargneRepository repository;

    @InjectMock
    CompteEpargneRepository compteEpargneRepository;

    @InjectMock
    TransactionEpargneMapper mapper;

    @InjectMock
    ParametresLcbFtRepository parametresLcbFtRepository;

    @InjectMock
    AuditService auditService;

    @InjectMock
    AlerteLcbFtService alerteLcbFtService;

    @BeforeEach
    void setupMocks() {
        // Default LCB-FT threshold: 500_000 XOF
        when(parametresLcbFtRepository.getSeuilJustification(any(), any()))
                .thenReturn(Optional.of(new BigDecimal("500000")));
        // AuditService and AlerteLcbFtService are void-returning — Mockito mocks by default do nothing
    }

    private CompteEpargne makeCompteActif(UUID compteId, BigDecimal solde) {
        CompteEpargne compte = new CompteEpargne();
        compte.setId(compteId);
        compte.setStatut(StatutCompteEpargne.ACTIF);
        compte.setSoldeActuel(solde);
        compte.setSoldeBloque(BigDecimal.ZERO);
        return compte;
    }

    @Test
    @DisplayName("executerTransaction DEPOT augmente le solde")
    void executerTransaction_depot_increasesBalance() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("1000"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("500"))
                .build();

        TransactionEpargne entity = new TransactionEpargne();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("1500");
        verify(repository).persist(any(TransactionEpargne.class));
    }

    @Test
    @DisplayName("executerTransaction RETRAIT échoue si solde insuffisant")
    void executerTransaction_retrait_failsIfInsufficientBalance() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("100"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.RETRAIT)
                .montant(new BigDecimal("500"))
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));

        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("Solde disponible insuffisant");
    }

    @Test
    @DisplayName("executerTransaction RETRAIT réussit si solde suffisant")
    void executerTransaction_retrait_succeeds() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("1000"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.RETRAIT)
                .montant(new BigDecimal("300"))
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("700");
    }

    @Test
    @DisplayName("executerTransaction sur compte non actif lance IllegalArgumentException")
    void executerTransaction_compteNonActif_throws() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = new CompteEpargne();
        compte.setId(compteId);
        compte.setStatut(StatutCompteEpargne.CLOTURE);
        compte.setSoldeActuel(new BigDecimal("1000"));
        compte.setSoldeBloque(BigDecimal.ZERO);

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("100"))
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));

        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("non actif");
    }

    @Test
    @DisplayName("executerTransaction avec compte inexistant lance NotFoundException")
    void executerTransaction_compteInexistant_throws() {
        UUID compteId = UUID.randomUUID();

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("100"))
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("executerTransaction PAIEMENT_INTERETS augmente le solde")
    void executerTransaction_paiementInterets_increasesBalance() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("2000"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.PAIEMENT_INTERETS)
                .montant(new BigDecimal("100"))
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("2100");
    }

    @Test
    @DisplayName("executerTransaction PRELEVEMENT_FRAIS diminue le solde")
    void executerTransaction_prelevementFrais_decreasesBalance() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("500"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.PRELEVEMENT_FRAIS)
                .montant(new BigDecimal("50"))
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("450");
    }

    @Test
    @DisplayName("executerTransaction RETENUE_GARANTIE bloque le solde sans le réduire")
    void executerTransaction_retenueGarantie_blocksSolde() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("1000"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.RETENUE_GARANTIE)
                .montant(new BigDecimal("200"))
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("1000"); // unchanged
        assertThat(compte.getSoldeBloque()).isEqualByComparingTo("200");
    }

    @Test
    @DisplayName("executerTransaction RETENUE_GARANTIE échoue si solde disponible insuffisant")
    void executerTransaction_retenueGarantie_insufficientDisponible_throws() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("100"));
        compte.setSoldeBloque(new BigDecimal("90")); // only 10 available

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.RETENUE_GARANTIE)
                .montant(new BigDecimal("50"))
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));

        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("Solde disponible insuffisant");
    }

    @Test
    @DisplayName("executerTransaction LIBERATION_GARANTIE débloque le solde")
    void executerTransaction_liberationGarantie_unblocksSolde() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("1000"));
        compte.setSoldeBloque(new BigDecimal("300"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.LIBERATION_GARANTIE)
                .montant(new BigDecimal("100"))
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("1000"); // unchanged
        assertThat(compte.getSoldeBloque()).isEqualByComparingTo("200");
    }

    @Test
    @DisplayName("executerTransaction LIBERATION_GARANTIE échoue si montant supérieur au solde bloqué")
    void executerTransaction_liberationGarantie_montantTropGrand_throws() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("1000"));
        compte.setSoldeBloque(new BigDecimal("50"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.LIBERATION_GARANTIE)
                .montant(new BigDecimal("100"))
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));

        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("solde bloqué");
    }

    @Test
    @DisplayName("executerTransaction avec montant >= seuil sans origineFonds lance IllegalArgumentException")
    void executerTransaction_montantAuSeuil_sansOrigineFonds_throws() {
        UUID compteId = UUID.randomUUID();
        // Default threshold is 500_000 XOF; use a big amount to trigger LCB-FT
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("2000000"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("500000")) // equals the default threshold
                .origineFonds(null) // missing -> should fail
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));

        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    @DisplayName("executerTransaction avec montant >= seuil et origineFonds valide réussit")
    void executerTransaction_montantAuSeuil_avecOrigineFonds_succeeds() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("2000000"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("500000"))
                .origineFonds("Salaire mensuel")
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("2500000");
    }

    @Test
    @DisplayName("transferer avec compteDestinationId null lance IllegalArgumentException")
    void transferer_compteDestinationNull_throws() {
        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(UUID.randomUUID().toString())
                .typeTransaction(TypeTransactionEpargne.TRANSFERT_SORTANT)
                .montant(new BigDecimal("100"))
                .compteDestinationId(null)
                .build();

        assertThatThrownBy(() -> service.transferer(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("destination");
    }

    @Test
    @DisplayName("transferer avec compteId == compteDestinationId lance IllegalArgumentException")
    void transferer_memeCompte_throws() {
        String sameId = UUID.randomUUID().toString();
        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(sameId)
                .typeTransaction(TypeTransactionEpargne.TRANSFERT_SORTANT)
                .montant(new BigDecimal("100"))
                .compteDestinationId(sameId)
                .build();

        assertThatThrownBy(() -> service.transferer(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("différents");
    }

    @Test
    @DisplayName("transferer avec deux comptes distincts exécute débit et crédit")
    void transferer_validTransfer_executesDebitAndCredit() {
        UUID sourceId = UUID.randomUUID();
        UUID destId = UUID.randomUUID();

        CompteEpargne source = makeCompteActif(sourceId, new BigDecimal("1000"));
        CompteEpargne dest = makeCompteActif(destId, new BigDecimal("500"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(sourceId.toString())
                .compteDestinationId(destId.toString())
                .typeTransaction(TypeTransactionEpargne.TRANSFERT_SORTANT)
                .montant(new BigDecimal("300"))
                .motif("Remboursement")
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(sourceId)).thenReturn(Optional.of(source));
        when(compteEpargneRepository.findByIdOptional(destId)).thenReturn(Optional.of(dest));
        when(mapper.toEntity(any())).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.transferer(request);

        // Source solde should have decreased
        assertThat(source.getSoldeActuel()).isEqualByComparingTo("700");
        // Destination solde should have increased
        assertThat(dest.getSoldeActuel()).isEqualByComparingTo("800");
    }

    @Test
    @DisplayName("getTransactionsByCompte retourne la liste des transactions")
    void getTransactionsByCompte_returnsList() {
        UUID compteId = UUID.randomUUID();
        TransactionEpargne tx = new TransactionEpargne();
        TransactionEpargneResponse dto = TransactionEpargneResponse.builder().build();

        when(repository.find("compte.id = ?1 ORDER BY dateTransaction DESC", compteId))
                .thenReturn(mock(io.quarkus.hibernate.orm.panache.PanacheQuery.class));
        // Since this uses stream(), mock the query result
        io.quarkus.hibernate.orm.panache.PanacheQuery<TransactionEpargne> mockQuery =
                mock(io.quarkus.hibernate.orm.panache.PanacheQuery.class);
        when(mockQuery.stream()).thenReturn(List.of(tx).stream());
        when(repository.find("compte.id = ?1 ORDER BY dateTransaction DESC", compteId)).thenReturn(mockQuery);
        when(mapper.toDto(tx)).thenReturn(dto);

        List<TransactionEpargneResponse> result = service.getTransactionsByCompte(compteId);
        assertThat(result).hasSize(1);
    }

    @Test
    @DisplayName("executerTransaction REMBOURSEMENT_CREDIT diminue le solde")
    void executerTransaction_remboursementCredit_decreasesBalance() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("800"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.REMBOURSEMENT_CREDIT)
                .montant(new BigDecimal("200"))
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("600");
    }

    // -------------------------------------------------------------------------
    // Branches manquantes dans validerLcbFtSiSeuilAtteint
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("validerLcbFtSiSeuilAtteint — montant null retourne sans erreur")
    void executerTransaction_montantNull_passesLcbFtValidation() {
        // montant null → validerLcbFtSiSeuilAtteint retourne immédiatement
        // Le compte est non-actif pour que le flow s'arrête avant le null-pointer sur montant
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = new CompteEpargne();
        compte.setId(compteId);
        compte.setStatut(StatutCompteEpargne.CLOTURE);
        compte.setSoldeActuel(new BigDecimal("1000"));
        compte.setSoldeBloque(java.math.BigDecimal.ZERO);

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(null)
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));

        // montant=null → validerLcbFtSiSeuilAtteint returns early → then hits "non actif" check
        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("non actif");
    }

    @Test
    @DisplayName("validerLcbFtSiSeuilAtteint — seuil null (parametresLcbFt retourne empty sans default) retourne sans erreur")
    void executerTransaction_seuilNull_passesLcbFtValidation() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = new CompteEpargne();
        compte.setId(compteId);
        compte.setStatut(StatutCompteEpargne.CLOTURE);
        compte.setSoldeActuel(new BigDecimal("2000000"));
        compte.setSoldeBloque(java.math.BigDecimal.ZERO);

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("1000000"))
                .origineFonds(null)
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        // Override threshold to be very large so montant < seuil → no LCB-FT check fires.
        // This covers the compareTo < 0 branch in validerLcbFtSiSeuilAtteint.
        when(parametresLcbFtRepository.getSeuilJustification(any(), any()))
                .thenReturn(Optional.of(new BigDecimal("9999999")));

        // montant 1_000_000 < seuil 9_999_999 → validerLcbFtSiSeuilAtteint: compareTo < 0, no validation
        // compte CLOTURE → hits "non actif" exception
        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("non actif");
    }

    @Test
    @DisplayName("validerLcbFtSiSeuilAtteint — origineFonds trop long lance IllegalArgumentException")
    void executerTransaction_origineFondsTropLong_throws() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("2000000"));

        // origineFonds dépasse ORIGINE_FONDS_MAX_LENGTH (500 chars > 255)
        String longOrigineFonds = "A".repeat(500);
        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("500000"))
                .origineFonds(longOrigineFonds)
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));

        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    @DisplayName("executerTransaction avec montant >= seuil, compte sans organisation — auditService appelé avec orgId=null")
    void executerTransaction_montantAuSeuil_sansOrganisation_auditWithNullOrgId() {
        UUID compteId = UUID.randomUUID();
        // Compte sans organisation ni membre
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("2000000"));
        // organisation et membre sont null (makeCompteActif ne les positionne pas)

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("500000"))
                .origineFonds("Salaire")
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        // auditService et alerteLcbFtService doivent être appelés avec orgId=null, membreId=null
        verify(auditService).logLcbFtSeuilAtteint(
                isNull(), any(), any(), any(), any(), any());
        verify(alerteLcbFtService).genererAlerteSeuilDepasse(
                isNull(), isNull(), any(), any(), any(), any(), any());
    }

    @Test
    @DisplayName("executerTransaction avec pieceJustificativeId valide — UUID parsé et affecté")
    void executerTransaction_avecPieceJustificativeId_parsed() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("1000"));
        UUID pieceId = UUID.randomUUID();

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("100"))
                .pieceJustificativeId(pieceId.toString())
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(entity.getPieceJustificativeId()).isEqualTo(pieceId);
        verify(repository).persist(entity);
    }

    @Test
    @DisplayName("executerTransaction avec dateTransaction déjà définie — ne réassigne pas")
    void executerTransaction_dateTransactionDejaDefinie_notOverridden() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("1000"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("100"))
                .build();

        LocalDateTime dateFixe = LocalDateTime.of(2025, 1, 1, 12, 0);
        TransactionEpargne entity = new TransactionEpargne();
        entity.setDateTransaction(dateFixe); // déjà défini avant persist

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        // dateTransaction déjà défini → la branche `if (null)` est false → date reste la même
        assertThat(entity.getDateTransaction()).isEqualTo(dateFixe);
    }

    @Test
    @DisplayName("executerTransaction TRANSFERT_ENTRANT augmente le solde")
    void executerTransaction_transfertEntrant_increasesBalance() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("500"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.TRANSFERT_ENTRANT)
                .montant(new BigDecimal("200"))
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        assertThat(compte.getSoldeActuel()).isEqualByComparingTo("700");
    }

    /**
     * Couvre les branches non-null dans le bloc LCB-FT :
     * - L129 : compte.getOrganisation() != null → getId() branch
     * - L135 : transaction.getId() != null → toString() branch
     * - L140 : compte.getMembre() != null → getId() branch
     * - L144 : request.getTypeTransaction() != null → name() branch
     * - L147 : transaction.getId() != null → toString() branch (second occurrence)
     */
    @Test
    @DisplayName("LCB-FT seuil dépassé avec organisation et membre non-null → branches getId()/name() couvertes")
    void executerTransaction_lcbFtAvecOrganisationEtMembre_couvreNonNullBranches() {
        UUID compteId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());

        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());

        CompteEpargne compte = makeCompteActif(compteId, BigDecimal.ZERO);
        compte.setMembre(membre);
        compte.setOrganisation(org);

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("600000")) // > seuil 500000 → bloc LCB-FT
                .origineFonds("Salaire mensuel")
                .build();

        TransactionEpargne entity = new TransactionEpargne();
        // Simuler que persist() affecte un ID → couvre les branches toString() de transaction.getId()
        doAnswer(inv -> {
            TransactionEpargne t = inv.getArgument(0);
            t.setId(UUID.randomUUID());
            return null;
        }).when(repository).persist(any(TransactionEpargne.class));

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.executerTransaction(request);

        // Vérifier que les services LCB-FT ont été appelés
        verify(auditService).logLcbFtSeuilAtteint(any(), any(), any(), any(), any(), any());
        verify(alerteLcbFtService).genererAlerteSeuilDepasse(any(), any(), any(), any(), any(), any(), any());
    }

    @Test
    @DisplayName("validerLcbFtSiSeuilAtteint — origineFonds blank (non-null mais espaces) lance IllegalArgumentException (L233 isBlank)")
    void executerTransaction_origineFondsBlank_throwsIllegalArgument() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = makeCompteActif(compteId, new BigDecimal("2000000"));

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("500000")) // >= seuil → entre dans bloc validation
                .origineFonds("   ") // non-null mais isBlank = true → L233 true → throw
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));

        assertThatThrownBy(() -> service.executerTransaction(request))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    @DisplayName("transferer avec compteDestinationId vide (empty) lance IllegalArgumentException")
    void transferer_compteDestinationEmpty_throws() {
        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(UUID.randomUUID().toString())
                .typeTransaction(TypeTransactionEpargne.TRANSFERT_SORTANT)
                .montant(new BigDecimal("100"))
                .compteDestinationId("") // empty string
                .build();

        assertThatThrownBy(() -> service.transferer(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("destination");
    }
}
