package dev.lions.unionflow.server.repository;

import dev.lions.unionflow.server.entity.Cotisation;
import io.quarkus.panache.common.Sort;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * Tests unitaires purs pour CotisationRepository.
 *
 * <p>Ces tests couvrent les branches ternaires {@code result != null ? result : fallback}
 * dans les méthodes d'agrégation — branches qui ne peuvent être atteintes en test d'intégration
 * car COUNT/SUM via JPQL ne retournent jamais null via getSingleResult() avec H2.
 *
 * <p>On instancie CotisationRepository directement (sans CDI) et on injecte un EntityManager
 * mocké par réflexion.
 */
@DisplayName("CotisationRepository — tests unitaires (branches ternaires null)")
class CotisationRepositoryUnitTest {

    /**
     * Sous-classe concrète non gérée par CDI — permet d'instancier CotisationRepository
     * sans le contexte Quarkus/CDI.
     */
    static class TestCotisationRepository extends CotisationRepository {
        TestCotisationRepository(EntityManager em) {
            super();
            this.entityManager = em;
        }
    }

    private EntityManager em;
    private TestCotisationRepository repo;

    @BeforeEach
    void setUp() {
        em = mock(EntityManager.class);
        repo = new TestCotisationRepository(em);
    }

    // ─── Helpers ──────────────────────────────────────────────────────────────

    @SuppressWarnings("unchecked")
    private TypedQuery<Long> mockLongQuery(Long returnValue) {
        TypedQuery<Long> q = mock(TypedQuery.class);
        when(q.setParameter(anyString(), any())).thenReturn(q);
        when(q.getSingleResult()).thenReturn(returnValue);
        return q;
    }

    @SuppressWarnings("unchecked")
    private TypedQuery<BigDecimal> mockBigDecimalQuery(BigDecimal returnValue) {
        TypedQuery<BigDecimal> q = mock(TypedQuery.class);
        when(q.setParameter(anyString(), any())).thenReturn(q);
        when(q.getSingleResult()).thenReturn(returnValue);
        return q;
    }

    @SuppressWarnings("unchecked")
    private TypedQuery<Cotisation> mockCotisationQuery(List<Cotisation> results) {
        TypedQuery<Cotisation> q = mock(TypedQuery.class);
        when(q.setParameter(anyString(), any())).thenReturn(q);
        when(q.setFirstResult(anyInt())).thenReturn(q);
        when(q.setMaxResults(anyInt())).thenReturn(q);
        when(q.getResultList()).thenReturn(results);
        return q;
    }

    // ─── countByMembreId ──────────────────────────────────────────────────────

    @Test
    @DisplayName("countByMembreId: result null → retourne 0L (branche ternaire null)")
    void countByMembreId_resultNull_returnsZero() {
        TypedQuery<Long> q = mockLongQuery(null);
        when(em.createQuery(contains("COUNT(c) FROM Cotisation c WHERE c.membre.id"), eq(Long.class)))
                .thenReturn(q);

        long result = repo.countByMembreId(UUID.randomUUID());
        assertThat(result).isEqualTo(0L);
    }

    @Test
    @DisplayName("countByMembreId: result non-null → retourne la valeur (branche ternaire non-null)")
    void countByMembreId_resultNonNull_returnsValue() {
        TypedQuery<Long> q = mockLongQuery(5L);
        when(em.createQuery(contains("COUNT(c) FROM Cotisation c WHERE c.membre.id"), eq(Long.class)))
                .thenReturn(q);

        long result = repo.countByMembreId(UUID.randomUUID());
        assertThat(result).isEqualTo(5L);
    }

    @Test
    @DisplayName("countByMembreId: membreId null → retourne 0L immédiatement (guard clause)")
    void countByMembreId_nullId_returnsZeroImmediately() {
        long result = repo.countByMembreId(null);
        assertThat(result).isEqualTo(0L);
        verifyNoInteractions(em);
    }

    // ─── countPayeesByMembreId ─────────────────────────────────────────────────

    @Test
    @DisplayName("countPayeesByMembreId: result null → retourne 0L")
    void countPayeesByMembreId_resultNull_returnsZero() {
        TypedQuery<Long> q = mockLongQuery(null);
        when(em.createQuery(contains("COUNT(c) FROM Cotisation c WHERE c.membre.id"), eq(Long.class)))
                .thenReturn(q);

        long result = repo.countPayeesByMembreId(UUID.randomUUID());
        assertThat(result).isEqualTo(0L);
    }

    @Test
    @DisplayName("countPayeesByMembreId: result non-null → retourne la valeur (branche ternaire non-null)")
    void countPayeesByMembreId_resultNonNull_returnsValue() {
        TypedQuery<Long> q = mockLongQuery(3L);
        when(em.createQuery(contains("COUNT(c) FROM Cotisation c WHERE c.membre.id"), eq(Long.class)))
                .thenReturn(q);

        long result = repo.countPayeesByMembreId(UUID.randomUUID());
        assertThat(result).isEqualTo(3L);
    }

    @Test
    @DisplayName("countPayeesByMembreId: membreId null → retourne 0L")
    void countPayeesByMembreId_nullId_returnsZero() {
        long result = repo.countPayeesByMembreId(null);
        assertThat(result).isEqualTo(0L);
        verifyNoInteractions(em);
    }

    // ─── countByOrganisationId ─────────────────────────────────────────────────

    @Test
    @DisplayName("countByOrganisationId: result null → retourne 0L")
    void countByOrganisationId_resultNull_returnsZero() {
        TypedQuery<Long> q = mockLongQuery(null);
        when(em.createQuery(contains("COUNT(c) FROM Cotisation c WHERE c.organisation.id"), eq(Long.class)))
                .thenReturn(q);

        long result = repo.countByOrganisationId(UUID.randomUUID());
        assertThat(result).isEqualTo(0L);
    }

    @Test
    @DisplayName("countByOrganisationId: organisationId null → retourne 0L")
    void countByOrganisationId_nullId_returnsZero() {
        long result = repo.countByOrganisationId(null);
        assertThat(result).isEqualTo(0L);
        verifyNoInteractions(em);
    }

    // ─── countByOrganisationIdAndDatePaiementBetween ───────────────────────────

    @Test
    @DisplayName("countByOrganisationIdAndDatePaiementBetween: result null → retourne 0L")
    void countByOrgIdAndDateBetween_resultNull_returnsZero() {
        TypedQuery<Long> q = mockLongQuery(null);
        when(em.createQuery(contains("COUNT(c) FROM Cotisation c WHERE c.organisation.id"), eq(Long.class)))
                .thenReturn(q);

        long result = repo.countByOrganisationIdAndDatePaiementBetween(
                UUID.randomUUID(), LocalDateTime.now().minusDays(1), LocalDateTime.now());
        assertThat(result).isEqualTo(0L);
    }

    @Test
    @DisplayName("countByOrganisationIdAndDatePaiementBetween: organisationId null → retourne 0L")
    void countByOrgIdAndDateBetween_nullId_returnsZero() {
        long result = repo.countByOrganisationIdAndDatePaiementBetween(
                null, LocalDateTime.now().minusDays(1), LocalDateTime.now());
        assertThat(result).isEqualTo(0L);
        verifyNoInteractions(em);
    }

    // ─── calculerTotalCotisationsPayeesCeMois ─────────────────────────────────

    @Test
    @DisplayName("calculerTotalCotisationsPayeesCeMois: total null → retourne ZERO")
    void calculerTotalPayeesCeMois_totalNull_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("SUM(c.montantPaye) FROM Cotisation c WHERE c.membre.id"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalCotisationsPayeesCeMois(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("calculerTotalCotisationsPayeesCeMois: total non-null → retourne la valeur")
    void calculerTotalPayeesCeMois_totalNonNull_returnsValue() {
        BigDecimal expected = BigDecimal.valueOf(5000);
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(expected);
        when(em.createQuery(contains("SUM(c.montantPaye) FROM Cotisation c WHERE c.membre.id"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalCotisationsPayeesCeMois(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(expected);
    }

    // ─── countRetardByMembreId ─────────────────────────────────────────────────

    @Test
    @DisplayName("countRetardByMembreId: count null → retourne 0L")
    void countRetardByMembreId_countNull_returnsZero() {
        TypedQuery<Long> q = mockLongQuery(null);
        when(em.createQuery(contains("COUNT(c) FROM Cotisation c WHERE c.membre.id"), eq(Long.class)))
                .thenReturn(q);

        long result = repo.countRetardByMembreId(UUID.randomUUID());
        assertThat(result).isEqualTo(0L);
    }

    @Test
    @DisplayName("countRetardByMembreId: count non-null → retourne la valeur")
    void countRetardByMembreId_countNonNull_returnsValue() {
        TypedQuery<Long> q = mockLongQuery(3L);
        when(em.createQuery(contains("COUNT(c) FROM Cotisation c WHERE c.membre.id"), eq(Long.class)))
                .thenReturn(q);

        long result = repo.countRetardByMembreId(UUID.randomUUID());
        assertThat(result).isEqualTo(3L);
    }

    // ─── calculerTotalCotisationsPayeesAnneeEnCours ───────────────────────────

    @Test
    @DisplayName("calculerTotalCotisationsPayeesAnneeEnCours: total null → retourne ZERO")
    void calculerTotalPayeesAnnee_totalNull_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("SUM(c.montantPaye) FROM Cotisation c WHERE c.membre.id"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalCotisationsPayeesAnneeEnCours(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("calculerTotalCotisationsPayeesAnneeEnCours: total non-null → retourne la valeur")
    void calculerTotalPayeesAnnee_totalNonNull_returnsValue() {
        BigDecimal expected = BigDecimal.valueOf(12000);
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(expected);
        when(em.createQuery(contains("SUM(c.montantPaye) FROM Cotisation c WHERE c.membre.id"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalCotisationsPayeesAnneeEnCours(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(expected);
    }

    // ─── calculerTotalCotisationsPayeesToutTemps ──────────────────────────────

    @Test
    @DisplayName("calculerTotalCotisationsPayeesToutTemps: total null → retourne ZERO")
    void calculerTotalPayeesToutTemps_totalNull_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("SUM(c.montantPaye) FROM Cotisation c WHERE c.membre.id"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalCotisationsPayeesToutTemps(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("calculerTotalCotisationsPayeesToutTemps: total non-null → retourne la valeur")
    void calculerTotalPayeesToutTemps_totalNonNull_returnsValue() {
        BigDecimal expected = BigDecimal.valueOf(25000);
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(expected);
        when(em.createQuery(contains("SUM(c.montantPaye) FROM Cotisation c WHERE c.membre.id"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalCotisationsPayeesToutTemps(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(expected);
    }

    // ─── calculerTotalMontantDu ────────────────────────────────────────────────

    @Test
    @DisplayName("calculerTotalMontantDu: result null → retourne ZERO")
    void calculerTotalMontantDu_resultNull_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("COALESCE(SUM(c.montantDu)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalMontantDu(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    // ─── calculerTotalMontantPaye ─────────────────────────────────────────────

    @Test
    @DisplayName("calculerTotalMontantPaye: result null → retourne ZERO")
    void calculerTotalMontantPaye_resultNull_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("COALESCE(SUM(c.montantPaye)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalMontantPaye(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    // ─── sommeMontantPayeParStatut ─────────────────────────────────────────────

    @Test
    @DisplayName("sommeMontantPayeParStatut: result null → retourne ZERO")
    void sommeMontantPayeParStatut_resultNull_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("COALESCE(SUM(c.montantPaye)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.sommeMontantPayeParStatut("PAYEE");
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("sommeMontantPayeParStatut: result non-null → retourne la valeur")
    void sommeMontantPayeParStatut_resultNonNull_returnsValue() {
        BigDecimal expected = BigDecimal.valueOf(8000);
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(expected);
        when(em.createQuery(contains("COALESCE(SUM(c.montantPaye)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.sommeMontantPayeParStatut("PAYEE");
        assertThat(result).isEqualByComparingTo(expected);
    }

    // ─── getStatistiquesPeriode ────────────────────────────────────────────────

    @Test
    @DisplayName("getStatistiquesPeriode avec mois: totalCotisations null → tauxPaiement=0.0")
    void getStatistiquesPeriode_withMois_nullTotal_tauxZero() {
        int annee = LocalDate.now().getYear();
        int mois = LocalDate.now().getMonthValue();

        // Un seul mock Long pour toutes les requêtes COUNT (countQuery + payeesQuery)
        TypedQuery<Long> nullLongQ = mockLongQuery(null);
        // Un seul mock BigDecimal pour toutes les requêtes SUM
        TypedQuery<BigDecimal> nullBdQ = mockBigDecimalQuery(null);

        when(em.createQuery(anyString(), eq(Long.class))).thenReturn(nullLongQ);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(nullBdQ);

        Map<String, Object> stats = repo.getStatistiquesPeriode(annee, mois);
        assertThat(stats).containsKey("tauxPaiement");
        assertThat((Double) stats.get("tauxPaiement")).isEqualTo(0.0);
        assertThat(stats.get("totalCotisations")).isEqualTo(0L);
        assertThat(stats.get("montantTotal")).isEqualTo(BigDecimal.ZERO);
        assertThat(stats.get("montantPaye")).isEqualTo(BigDecimal.ZERO);
        assertThat(stats.get("cotisationsPayees")).isEqualTo(0L);
    }

    @Test
    @DisplayName("getStatistiquesPeriode sans mois: totalCotisations null → tauxPaiement=0.0")
    void getStatistiquesPeriode_withoutMois_nullTotal_tauxZero() {
        int annee = LocalDate.now().getYear();

        TypedQuery<Long> nullLongQ = mockLongQuery(null);
        TypedQuery<BigDecimal> nullBdQ = mockBigDecimalQuery(null);

        when(em.createQuery(anyString(), eq(Long.class))).thenReturn(nullLongQ);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(nullBdQ);

        Map<String, Object> stats = repo.getStatistiquesPeriode(annee, null);
        assertThat(stats).containsKey("tauxPaiement");
        assertThat((Double) stats.get("tauxPaiement")).isEqualTo(0.0);
    }

    @Test
    @DisplayName("getStatistiquesPeriode: totalCotisations > 0 → taux calculé (branche totalCotisations > 0)")
    void getStatistiquesPeriode_nonZeroTotal_tauxCalcule() {
        int annee = LocalDate.now().getYear();

        // totalCotisations = 10, cotisationsPayees = 8 → taux = 80.0
        TypedQuery<Long> countQ = mockLongQuery(10L);
        TypedQuery<Long> payeesQ = mockLongQuery(8L);
        TypedQuery<BigDecimal> montantTotalQ = mockBigDecimalQuery(BigDecimal.valueOf(100000));
        TypedQuery<BigDecimal> montantPayeQ = mockBigDecimalQuery(BigDecimal.valueOf(80000));

        // getStatistiquesPeriode(annee, null) branche else : crée countQuery en premier,
        // puis montantTotalQuery, montantPayeQuery, payeesQuery — dans cet ordre pour Long.
        // On utilise thenReturn avec cascade : 1er appel → countQ(10), 2ème → payeesQ(8)
        when(em.createQuery(anyString(), eq(Long.class)))
                .thenReturn(countQ)
                .thenReturn(payeesQ);
        when(em.createQuery(anyString(), eq(BigDecimal.class)))
                .thenReturn(montantTotalQ)
                .thenReturn(montantPayeQ);

        Map<String, Object> stats = repo.getStatistiquesPeriode(annee, null);
        assertThat(stats).containsKey("tauxPaiement");
        double taux = (double) stats.get("tauxPaiement");
        assertThat(taux).isEqualTo(80.0);
    }

    @Test
    @DisplayName("getStatistiquesPeriode avec mois: totalCotisations > 0 → taux calculé (branche totalCotisations > 0)")
    void getStatistiquesPeriode_withMois_nonZeroTotal_tauxCalcule() {
        int annee = LocalDate.now().getYear();
        int mois = LocalDate.now().getMonthValue();

        TypedQuery<Long> countQ = mockLongQuery(5L);
        TypedQuery<Long> payeesQ = mockLongQuery(4L);
        TypedQuery<BigDecimal> montantTotalQ = mockBigDecimalQuery(BigDecimal.valueOf(50000));
        TypedQuery<BigDecimal> montantPayeQ = mockBigDecimalQuery(BigDecimal.valueOf(40000));

        when(em.createQuery(anyString(), eq(Long.class)))
                .thenReturn(countQ)
                .thenReturn(payeesQ);
        when(em.createQuery(anyString(), eq(BigDecimal.class)))
                .thenReturn(montantTotalQ)
                .thenReturn(montantPayeQ);

        Map<String, Object> stats = repo.getStatistiquesPeriode(annee, mois);
        assertThat(stats).containsKey("tauxPaiement");
        double taux = (double) stats.get("tauxPaiement");
        assertThat(taux).isEqualTo(80.0);
    }

    // ─── sumMontantsPayes ─────────────────────────────────────────────────────

    @Test
    @DisplayName("sumMontantsPayes: result null → retourne ZERO")
    void sumMontantsPayes_resultNull_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("COALESCE(SUM(c.montantPaye)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.sumMontantsPayes(
                UUID.randomUUID(), LocalDateTime.now().minusDays(30), LocalDateTime.now());
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("sumMontantsPayes: result non-null → retourne la valeur")
    void sumMontantsPayes_resultNonNull_returnsValue() {
        BigDecimal expected = BigDecimal.valueOf(15000);
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(expected);
        when(em.createQuery(contains("COALESCE(SUM(c.montantPaye)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.sumMontantsPayes(
                UUID.randomUUID(), LocalDateTime.now().minusDays(30), LocalDateTime.now());
        assertThat(result).isEqualByComparingTo(expected);
    }

    // ─── sumMontantsEnAttente ─────────────────────────────────────────────────

    @Test
    @DisplayName("sumMontantsEnAttente: result null → retourne ZERO")
    void sumMontantsEnAttente_resultNull_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("COALESCE(SUM(c.montantDu)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.sumMontantsEnAttente(
                UUID.randomUUID(), LocalDateTime.now().minusDays(30), LocalDateTime.now());
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("sumMontantsEnAttente: result non-null → retourne la valeur")
    void sumMontantsEnAttente_resultNonNull_returnsValue() {
        BigDecimal expected = BigDecimal.valueOf(20000);
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(expected);
        when(em.createQuery(contains("COALESCE(SUM(c.montantDu)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.sumMontantsEnAttente(
                UUID.randomUUID(), LocalDateTime.now().minusDays(30), LocalDateTime.now());
        assertThat(result).isEqualByComparingTo(expected);
    }

    // ─── buildOrderBy (via appels publics) ───────────────────────────────────

    @Test
    @DisplayName("buildOrderBy: sort null → fallback 'c.dateEcheance DESC' (via findByMembreId)")
    void buildOrderBy_sortNull_returnsFallback() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.findByMembreId(UUID.randomUUID(), page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("buildOrderBy: sort non-null avec colonnes → construit ORDER BY (via findByMembreId)")
    void buildOrderBy_sortWithColumns_buildsOrderBy() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        Sort sort = Sort.descending("dateEcheance");
        List<Cotisation> result = repo.findByMembreId(UUID.randomUUID(), page, sort);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("buildOrderBy: sort non-null avec colonnes multiples → couvre la branche i>0")
    void buildOrderBy_multipleColumns_coversIPlusBranch() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        Sort sort = Sort.by("dateEcheance", Sort.Direction.Descending).and("statut", Sort.Direction.Ascending);
        List<Cotisation> result = repo.findByMembreId(UUID.randomUUID(), page, sort);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("buildOrderBy: sort non-null avec colonnes vides → fallback 'c.dateEcheance DESC' (branche isEmpty=true)")
    void buildOrderBy_emptySort_returnsFallback() throws Exception {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        // Créer un Sort non-null avec liste de colonnes vide via réflexion
        Sort emptySort;
        try {
            java.lang.reflect.Constructor<Sort> ctor = Sort.class.getDeclaredConstructor();
            ctor.setAccessible(true);
            emptySort = ctor.newInstance();
        } catch (NoSuchMethodException e) {
            // Sort n'a pas de constructeur no-arg : test skip
            return;
        }

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.findByMembreId(UUID.randomUUID(), page, emptySort);
        assertThat(result).isNotNull();
    }

    // ─── findByOrganisationIdIn ───────────────────────────────────────────────

    @Test
    @DisplayName("findByOrganisationIdIn: organisationIds null → retourne liste vide (branche null)")
    void findByOrganisationIdIn_nullIds_returnsEmptyList() {
        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.findByOrganisationIdIn(null, page, null);
        assertThat(result).isEmpty();
        verifyNoInteractions(em);
    }

    @Test
    @DisplayName("findByOrganisationIdIn: organisationIds vide → retourne liste vide (branche isEmpty)")
    void findByOrganisationIdIn_emptyIds_returnsEmptyList() {
        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.findByOrganisationIdIn(Set.of(), page, null);
        assertThat(result).isEmpty();
        verifyNoInteractions(em);
    }

    @Test
    @DisplayName("findByOrganisationIdIn: sort null → utilise ORDER BY dateEcheance DESC (branche sort null)")
    void findByOrganisationIdIn_withIds_sortNull_returnsList() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        java.util.Set<UUID> ids = Set.of(UUID.randomUUID());
        List<Cotisation> result = repo.findByOrganisationIdIn(ids, page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("findByOrganisationIdIn: sort non-null → utilise buildOrderBy (branche sort non-null)")
    void findByOrganisationIdIn_withIds_sortNonNull_returnsList() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        java.util.Set<UUID> ids = Set.of(UUID.randomUUID());
        Sort sort = Sort.descending("dateEcheance");
        List<Cotisation> result = repo.findByOrganisationIdIn(ids, page, sort);
        assertThat(result).isNotNull();
    }

    // ─── findEnAttenteByOrganisationIdIn ─────────────────────────────────────

    @Test
    @DisplayName("findEnAttenteByOrganisationIdIn: null → retourne liste vide")
    void findEnAttenteByOrganisationIdIn_nullIds_returnsEmptyList() {
        List<Cotisation> result = repo.findEnAttenteByOrganisationIdIn(null);
        assertThat(result).isEmpty();
        verifyNoInteractions(em);
    }

    @Test
    @DisplayName("findEnAttenteByOrganisationIdIn: vide → retourne liste vide")
    void findEnAttenteByOrganisationIdIn_emptyIds_returnsEmptyList() {
        List<Cotisation> result = repo.findEnAttenteByOrganisationIdIn(Set.of());
        assertThat(result).isEmpty();
        verifyNoInteractions(em);
    }

    @Test
    @DisplayName("findEnAttenteByOrganisationIdIn: ids valides → exécute la requête")
    void findEnAttenteByOrganisationIdIn_withIds_executesQuery() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        java.util.Set<UUID> ids = Set.of(UUID.randomUUID());
        List<Cotisation> result = repo.findEnAttenteByOrganisationIdIn(ids);
        assertThat(result).isNotNull();
    }

    // ─── searchAdvanced ───────────────────────────────────────────────────────

    @Test
    @DisplayName("searchAdvanced: tous paramètres null → WHERE 1=1, ORDER BY dateEcheance DESC")
    void searchAdvanced_allNull_returnsAll() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.searchAdvanced(null, null, null, null, page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchAdvanced: membreId non-null → ajoute filtre membre")
    void searchAdvanced_withMembreId_addsFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.searchAdvanced(UUID.randomUUID(), null, null, null, page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchAdvanced: statut non-null non-blank → ajoute filtre statut")
    void searchAdvanced_withStatut_addsFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.searchAdvanced(null, "PAYEE", null, null, page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchAdvanced: statut blank → pas de filtre statut (branche isBlank=true)")
    void searchAdvanced_withBlankStatut_noFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.searchAdvanced(null, "  ", null, null, page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchAdvanced: typeCotisation non-null non-blank → ajoute filtre type")
    void searchAdvanced_withTypeCotisation_addsFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.searchAdvanced(null, null, "ANNUELLE", null, page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchAdvanced: typeCotisation blank → pas de filtre type (branche isBlank=true)")
    void searchAdvanced_withBlankType_noFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.searchAdvanced(null, null, "", null, page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchAdvanced: annee non-null → ajoute filtre annee")
    void searchAdvanced_withAnnee_addsFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.searchAdvanced(null, null, null, 2024, page, null);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchAdvanced: sort non-null → utilise buildOrderBy (branche sort != null)")
    void searchAdvanced_withSort_usesOrderBy() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        Sort sort = Sort.descending("dateEcheance");
        List<Cotisation> result = repo.searchAdvanced(null, null, null, null, page, sort);
        assertThat(result).isNotNull();
    }

    // ─── calculerTotalCotisationsAnneeEnCours ─────────────────────────────────

    @Test
    @DisplayName("calculerTotalCotisationsAnneeEnCours: total null → retourne ZERO")
    void calculerTotalCotisationsAnneeEnCours_null_returnsZero() {
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(null);
        when(em.createQuery(contains("SUM(c.montantDu) FROM Cotisation c WHERE c.membre.id"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalCotisationsAnneeEnCours(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("calculerTotalCotisationsAnneeEnCours: total non-null → retourne la valeur")
    void calculerTotalCotisationsAnneeEnCours_nonNull_returnsValue() {
        BigDecimal expected = BigDecimal.valueOf(30000);
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(expected);
        when(em.createQuery(contains("SUM(c.montantDu) FROM Cotisation c WHERE c.membre.id"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalCotisationsAnneeEnCours(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(expected);
    }

    // ─── calculerTotalMontantPaye ─────────────────────────────────────────────

    @Test
    @DisplayName("calculerTotalMontantPaye: result non-null → retourne la valeur (branche non-null)")
    void calculerTotalMontantPaye_nonNull_returnsValue() {
        BigDecimal expected = BigDecimal.valueOf(7500);
        TypedQuery<BigDecimal> q = mockBigDecimalQuery(expected);
        when(em.createQuery(contains("COALESCE(SUM(c.montantPaye)"), eq(BigDecimal.class)))
                .thenReturn(q);

        BigDecimal result = repo.calculerTotalMontantPaye(UUID.randomUUID());
        assertThat(result).isEqualByComparingTo(expected);
    }

    // ─── incrementerNombreRappels ─────────────────────────────────────────────

    @Test
    @DisplayName("incrementerNombreRappels: cotisation non trouvée → retourne false")
    void incrementerNombreRappels_cotisationNotFound_returnsFalse() {
        when(em.find(eq(Cotisation.class), any())).thenReturn(null);

        boolean result = repo.incrementerNombreRappels(UUID.randomUUID());
        assertThat(result).isFalse();
    }

    // ─── rechercheAvancee branches manquantes ────────────────────────────────

    @Test
    @DisplayName("rechercheAvancee: statut non-null non-empty → ajoute filtre statut (branche isEmpty=false)")
    void rechercheAvancee_withStatut_addsFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.rechercheAvancee(null, "PAYEE", null, null, null, page);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("rechercheAvancee: typeCotisation non-null non-empty → ajoute filtre type (branche isEmpty=false)")
    void rechercheAvancee_withTypeCotisation_addsFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.rechercheAvancee(null, null, "MENSUELLE", null, null, page);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("rechercheAvancee: mois non-null → ajoute filtre mois (branche mois != null)")
    void rechercheAvancee_withMois_addsFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.rechercheAvancee(null, null, null, null, 6, page);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("rechercheAvancee: statut empty → pas de filtre statut (branche isEmpty=true)")
    void rechercheAvancee_withEmptyStatut_noFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.rechercheAvancee(null, "", null, null, null, page);
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("rechercheAvancee: typeCotisation empty → pas de filtre type (branche isEmpty=true)")
    void rechercheAvancee_withEmptyType_noFilter() {
        TypedQuery<Cotisation> q = mockCotisationQuery(List.of());
        when(em.createQuery(anyString(), eq(Cotisation.class))).thenReturn(q);

        io.quarkus.panache.common.Page page = new io.quarkus.panache.common.Page(0, 10);
        List<Cotisation> result = repo.rechercheAvancee(null, null, "", null, null, page);
        assertThat(result).isNotNull();
    }

    // ─── buildOrderBy (via réflexion) ─────────────────────────────────────────

    @Test
    @DisplayName("buildOrderBy: sort null → retourne 'c.dateEcheance DESC' (branche sort==null)")
    void buildOrderBy_sortNull_retourneDefault() throws Exception {
        java.lang.reflect.Method m = CotisationRepository.class.getDeclaredMethod("buildOrderBy", Sort.class);
        m.setAccessible(true);
        String result = (String) m.invoke(repo, (Sort) null);
        assertThat(result).isEqualTo("c.dateEcheance DESC");
    }

    @Test
    @DisplayName("buildOrderBy: sort non-null, columns vides → retourne 'c.dateEcheance DESC' (branche getColumns().isEmpty()=true)")
    void buildOrderBy_sortColumnsVides_retourneDefault() throws Exception {
        java.lang.reflect.Method m = CotisationRepository.class.getDeclaredMethod("buildOrderBy", Sort.class);
        m.setAccessible(true);
        Sort sort = org.mockito.Mockito.mock(Sort.class);
        when(sort.getColumns()).thenReturn(java.util.Collections.emptyList());
        String result = (String) m.invoke(repo, sort);
        assertThat(result).isEqualTo("c.dateEcheance DESC");
    }

    @Test
    @DisplayName("buildOrderBy: sort avec colonne ASC → 'c.nomColonne ASC' (branche direction != Descending)")
    void buildOrderBy_sortAvecColonneAsc_retourneAsc() throws Exception {
        java.lang.reflect.Method m = CotisationRepository.class.getDeclaredMethod("buildOrderBy", Sort.class);
        m.setAccessible(true);
        Sort sort = Sort.ascending("montantDu");
        String result = (String) m.invoke(repo, sort);
        assertThat(result).contains("ASC");
    }
}
