package dev.lions.unionflow.server.service;

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

import dev.lions.unionflow.server.api.dto.dashboard.RecentActivityResponse;
import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventResponse;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.DemandeAide;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.DemandeAideRepository;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;

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

/**
 * Tests de couverture complémentaires pour {@link DashboardServiceImpl}.
 *
 * <p>Branches ciblées :
 * <ul>
 *   <li>getRecentActivities:175 — chemin orgId==null (organizationId vide)</li>
 *   <li>lambda$getUpcomingEvents$4:265 — e.getOrganisation()==null quand orgId!=null</li>
 *   <li>calculateOrganizationTypeDistribution:327 — org avec type null → "Autre"</li>
 *   <li>convertToUpcomingEventResponse:272 — evenement.getStatut()==null → "PLANIFIE"</li>
 *   <li>calculateTotalContributionAmount:288 — résultat null → BigDecimal.ZERO</li>
 *   <li>calculateMonthlyContributionAmount:414 — organizationId==null → pas de param orgId</li>
 * </ul>
 */
@QuarkusTest
@DisplayName("DashboardServiceImpl — branches de couverture manquantes")
class DashboardServiceImplCoverageTest {

    @Inject
    DashboardServiceImpl dashboardService;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    EvenementRepository evenementRepository;

    @InjectMock
    CotisationRepository cotisationRepository;

    @InjectMock
    DemandeAideRepository demandeAideRepository;

    @InjectMock
    OrganisationRepository organisationRepository;

    private static final UUID ORG_ID = UUID.fromString("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");

    @BeforeEach
    void setupDefauts() {
        // Défauts pour éviter NullPointerException dans les chemins non testés
        when(membreRepository.rechercheAvancee(any(), any(), any(), any(), any(Page.class), any(Sort.class)))
                .thenReturn(Collections.emptyList());
        when(evenementRepository.listAll()).thenReturn(Collections.emptyList());
        when(cotisationRepository.rechercheAvancee(any(), anyString(), any(), any(), any(), any(Page.class)))
                .thenReturn(Collections.emptyList());
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(Collections.emptyList());
    }

    // =========================================================================
    // getRecentActivities — orgId == null (organizationId vide)
    // =========================================================================

    @Test
    @DisplayName("getRecentActivities avec organizationId vide → orgId null → filtre anyMatch skip (ligne 175, 3I 6B)")
    void getRecentActivities_orgIdNull_parcourt_sansOrgFiltre() {
        // Evenement SANS organisation → e.getOrganisation() == null → filtré hors de la liste
        Evenement evenementSansOrg = new Evenement();
        evenementSansOrg.setId(UUID.randomUUID());
        evenementSansOrg.setTitre("Event sans org");
        evenementSansOrg.setDateCreation(LocalDateTime.now());
        evenementSansOrg.setOrganisation(null); // organisation null

        // Evenement AVEC organisation mais ID différent → filtré
        Organisation orgDiff = new Organisation();
        orgDiff.setId(UUID.randomUUID());
        Evenement evenementAutreOrg = new Evenement();
        evenementAutreOrg.setId(UUID.randomUUID());
        evenementAutreOrg.setTitre("Event autre org");
        evenementAutreOrg.setDateCreation(LocalDateTime.now().minusHours(1));
        evenementAutreOrg.setOrganisation(orgDiff);

        when(evenementRepository.listAll()).thenReturn(List.of(evenementSansOrg, evenementAutreOrg));

        // organizationId vide → orgId = null
        List<RecentActivityResponse> activities = dashboardService.getRecentActivities("", "user@test.com", 10);

        // Avec orgId=null, le filtre e.getOrganisation().getId().equals(null) ne match rien
        // (car orgId=null → condition e.getOrganisation() != null && getId().equals(null) toujours fausse)
        assertThat(activities).isNotNull();
    }

    @Test
    @DisplayName("getRecentActivities avec organizationId invalide → orgId null → résultat vide")
    void getRecentActivities_orgIdInvalid_retourneListeVide() {
        List<RecentActivityResponse> activities = dashboardService.getRecentActivities(
                "not-a-uuid", "user@test.com", 5);
        assertThat(activities).isNotNull();
    }

    @Test
    @DisplayName("getRecentActivities avec cotisation dont le membre appartient à l'org → branche cotisation membre non null")
    void getRecentActivities_cotisationAvecMembre_orgMatch_ajouteLActivite() {
        Organisation org = new Organisation();
        org.setId(ORG_ID);

        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNom("Diallo");
        membre.setPrenom("Fatou");
        membre.setActif(true);

        Cotisation cotisation = new Cotisation();
        cotisation.setId(UUID.randomUUID());
        cotisation.setMembre(membre);          // membre != null
        cotisation.setOrganisation(org);       // org.getId().equals(ORG_ID) → match
        cotisation.setMontantPaye(new BigDecimal("5000"));
        cotisation.setCodeDevise("XOF");
        cotisation.setDatePaiement(LocalDateTime.now());
        cotisation.setDateCreation(LocalDateTime.now().minusHours(2));

        when(cotisationRepository.rechercheAvancee(isNull(), eq("PAYEE"), isNull(), isNull(), isNull(), any(Page.class)))
                .thenReturn(List.of(cotisation));

        List<RecentActivityResponse> activities = dashboardService.getRecentActivities(
                ORG_ID.toString(), "user@test.com", 10);

        // La cotisation liée à l'org est ajoutée comme activité "contribution"
        assertThat(activities).anyMatch(a -> "contribution".equals(a.getType()));
    }

    @Test
    @DisplayName("getRecentActivities avec cotisation datePaiement null → utilise dateCreation (branche ternaire)")
    void getRecentActivities_cotisationDatePaiementNull_utiliseDateCreation() {
        Organisation org = new Organisation();
        org.setId(ORG_ID);

        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNom("Traoré");
        membre.setPrenom("Moussa");
        membre.setActif(true);

        Cotisation cotisation = new Cotisation();
        cotisation.setId(UUID.randomUUID());
        cotisation.setMembre(membre);
        cotisation.setOrganisation(org);
        cotisation.setMontantPaye(BigDecimal.valueOf(3000));
        cotisation.setCodeDevise("XOF");
        cotisation.setDatePaiement(null);                            // null → ternaire: dateCreation
        cotisation.setDateCreation(LocalDateTime.now().minusDays(1));

        when(cotisationRepository.rechercheAvancee(isNull(), eq("PAYEE"), isNull(), isNull(), isNull(), any(Page.class)))
                .thenReturn(List.of(cotisation));

        List<RecentActivityResponse> activities = dashboardService.getRecentActivities(
                ORG_ID.toString(), "user@test.com", 10);

        assertThat(activities).anyMatch(a -> "contribution".equals(a.getType()));
    }

    // =========================================================================
    // getUpcomingEvents lambda:265 — e.getOrganisation()==null quand orgId!=null
    // =========================================================================

    @Test
    @DisplayName("getUpcomingEvents lambda:265 — événement sans organisation filtré quand orgId non null")
    void getUpcomingEvents_evenementSansOrg_filtreLorsqueOrgIdNonNull() {
        // Evenement avec organisation null → lambda:265 condition false → filtré
        Evenement evenementSansOrg = new Evenement();
        evenementSansOrg.setId(UUID.randomUUID());
        evenementSansOrg.setTitre("Event sans org");
        evenementSansOrg.setOrganisation(null);   // getOrganisation() == null → filter retourne false
        evenementSansOrg.setStatut("PLANIFIE");

        // Evenement avec organisation correspondante → passé
        Organisation org = new Organisation();
        org.setId(ORG_ID);
        Evenement evenementAvecOrg = new Evenement();
        evenementAvecOrg.setId(UUID.randomUUID());
        evenementAvecOrg.setTitre("Event avec org");
        evenementAvecOrg.setOrganisation(org);
        evenementAvecOrg.setStatut("PLANIFIE");
        evenementAvecOrg.setDateDebut(LocalDateTime.now().plusDays(1));

        when(evenementRepository.findEvenementsAVenirByOrganisationId(
                eq(ORG_ID), any(Page.class), any(Sort.class)))
                .thenReturn(List.of(evenementSansOrg, evenementAvecOrg));

        List<UpcomingEventResponse> events = dashboardService.getUpcomingEvents(
                ORG_ID.toString(), "user@test.com", 5);

        // Seul evenementAvecOrg passe le filtre lambda:265
        assertThat(events).hasSize(1);
        assertThat(events.get(0).getTitle()).isEqualTo("Event avec org");
    }

    @Test
    @DisplayName("getUpcomingEvents lambda:265 — orgId null → tous les événements passent le filtre")
    void getUpcomingEvents_orgIdNull_tousPassentLeFiltre() {
        Evenement evenement = new Evenement();
        evenement.setId(UUID.randomUUID());
        evenement.setTitre("Event global");
        evenement.setStatut("PLANIFIE");

        when(evenementRepository.findEvenementsAVenir(any(Page.class), any(Sort.class)))
                .thenReturn(List.of(evenement));

        // organizationId vide → orgId=null → filtre (orgId == null || ...) = true pour tous
        List<UpcomingEventResponse> events = dashboardService.getUpcomingEvents("", "user@test.com", 5);

        assertThat(events).hasSize(1);
    }

    // =========================================================================
    // convertToUpcomingEventResponse:272 — statut null → "PLANIFIE"
    // =========================================================================

    @Test
    @DisplayName("convertToUpcomingEventResponse — statut null → utilise 'PLANIFIE' (branche ternaire)")
    void convertToUpcomingEventResponse_statutNull_retournePlanifie() {
        Organisation org = new Organisation();
        org.setId(ORG_ID);

        Evenement evenement = new Evenement();
        evenement.setId(UUID.randomUUID());
        evenement.setTitre("Event statut null");
        evenement.setOrganisation(org);
        evenement.setStatut(null);  // null → ternaire retourne "PLANIFIE"

        when(evenementRepository.findEvenementsAVenirByOrganisationId(
                eq(ORG_ID), any(Page.class), any(Sort.class)))
                .thenReturn(List.of(evenement));

        List<UpcomingEventResponse> events = dashboardService.getUpcomingEvents(
                ORG_ID.toString(), "user@test.com", 5);

        assertThat(events).hasSize(1);
        assertThat(events.get(0).getStatus()).isEqualTo("PLANIFIE");
    }

    // =========================================================================
    // calculateOrganizationTypeDistribution:327 — type null → "Autre"
    // =========================================================================

    @Test
    @DisplayName("calculateOrganizationTypeDistribution — org avec type null → clé 'Autre'")
    void calculateOrganizationTypeDistribution_typeOrgNull_cleAutre() {
        // Appel via getDashboardStats avec orgId=null → branche else → calculateOrganizationTypeDistribution()
        when(membreRepository.count()).thenReturn(0L);
        when(membreRepository.countActifs()).thenReturn(0L);
        when(evenementRepository.count()).thenReturn(0L);
        when(evenementRepository.findEvenementsAVenir()).thenReturn(Collections.emptyList());
        when(cotisationRepository.count()).thenReturn(0L);
        when(organisationRepository.count()).thenReturn(1L);

        // Organisation avec type null → branche if (type == null || type.trim().isEmpty()) → "Autre"
        Organisation orgSansType = new Organisation();
        orgSansType.setId(UUID.randomUUID());
        orgSansType.setTypeOrganisation(null);

        Organisation orgTypeVide = new Organisation();
        orgTypeVide.setId(UUID.randomUUID());
        orgTypeVide.setTypeOrganisation("   ");  // blank → aussi "Autre"

        when(organisationRepository.listAll()).thenReturn(List.of(orgSansType, orgTypeVide));
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(Collections.emptyList());

        // Configurer EntityManager mock pour calculateTotalContributionAmount
        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(null); // null → BigDecimal.ZERO

        // Appel via getDashboardStats — orgId null
        when(membreRepository.countNouveauxMembres(any(LocalDate.class))).thenReturn(0L);

        var stats = dashboardService.getDashboardStats("", "user@test.com");

        // Distribution contiendra "Autre" pour les orgs sans type
        assertThat(stats.getOrganizationTypeDistribution()).containsKey("Autre");
    }

    @Test
    @DisplayName("calculateOrganizationTypeDistribution — org avec type non null → clé = type")
    void calculateOrganizationTypeDistribution_typeOrgNonNull_cle_avecType() {
        when(membreRepository.count()).thenReturn(0L);
        when(membreRepository.countActifs()).thenReturn(0L);
        when(evenementRepository.count()).thenReturn(0L);
        when(evenementRepository.findEvenementsAVenir()).thenReturn(Collections.emptyList());
        when(cotisationRepository.count()).thenReturn(0L);
        when(organisationRepository.count()).thenReturn(1L);

        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());
        org.setTypeOrganisation("MUTUELLE");

        when(organisationRepository.listAll()).thenReturn(List.of(org));
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(Collections.emptyList());

        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(BigDecimal.valueOf(1000));

        when(membreRepository.countNouveauxMembres(any(LocalDate.class))).thenReturn(0L);

        var stats = dashboardService.getDashboardStats("", "user@test.com");

        assertThat(stats.getOrganizationTypeDistribution()).containsKey("MUTUELLE");
    }

    // =========================================================================
    // calculateTotalContributionAmount:288 — result null → BigDecimal.ZERO
    // =========================================================================

    @Test
    @DisplayName("calculateTotalContributionAmount — résultat null → retourne BigDecimal.ZERO")
    void calculateTotalContributionAmount_resultNull_retourneZero() {
        // Appel via getDashboardStats avec orgId valide — calculateTotalContributionAmount(orgId) appelée
        when(membreRepository.countDistinctByOrganisationIdIn(any())).thenReturn(0L);
        when(membreRepository.countActifsDistinctByOrganisationIdIn(any())).thenReturn(0L);
        when(evenementRepository.countByOrganisationId(ORG_ID)).thenReturn(0L);
        when(evenementRepository.findEvenementsAVenirByOrganisationId(eq(ORG_ID), any(Page.class), any(Sort.class)))
                .thenReturn(Collections.emptyList());
        when(cotisationRepository.countByOrganisationId(ORG_ID)).thenReturn(0L);
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(Collections.emptyList());
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.empty());
        when(membreRepository.countNouveauxMembresByOrganisationId(any(LocalDate.class), eq(ORG_ID))).thenReturn(0L);
        when(membreRepository.countDistinctByOrganisationIdIn(any())).thenReturn(0L);
        when(membreRepository.countActifsDistinctByOrganisationIdIn(any())).thenReturn(0L);

        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        // Simule le cas où COALESCE retourne null (edge case) → branche result != null = false → ZERO
        when(query.getSingleResult()).thenReturn(null);

        var stats = dashboardService.getDashboardStats(ORG_ID.toString(), "user@test.com");

        // Le totalContributionAmount doit être 0.0 (BigDecimal.ZERO.doubleValue())
        assertThat(stats.getTotalContributionAmount()).isEqualTo(0.0);
    }

    // =========================================================================
    // calculateMonthlyContributionAmount:414 — organizationId null → pas de :orgId
    // =========================================================================

    // =========================================================================
    // lambda$getDashboardStats$0:121 — d.getOrganisation() == null → branche false
    // =========================================================================

    @Test
    @DisplayName("getDashboardStats lambda$0:121 — DemandeAide avec organisation null → filtre retourne false (branche getOrganisation()==null)")
    void getDashboardStats_demandeAideSansOrg_filtreRetourneFalse() {
        // Simule une DemandeAide EN_ATTENTE avec organisation null →
        // d.getOrganisation() == null → condition false → non comptée pour notre org
        DemandeAide demandeAvecOrgNull = new DemandeAide();
        demandeAvecOrgNull.setId(UUID.randomUUID());
        demandeAvecOrgNull.setOrganisation(null);  // null → branche d.getOrganisation()==null

        when(membreRepository.countDistinctByOrganisationIdIn(any())).thenReturn(10L);
        when(membreRepository.countActifsDistinctByOrganisationIdIn(any())).thenReturn(8L);
        when(evenementRepository.countByOrganisationId(ORG_ID)).thenReturn(5L);
        when(evenementRepository.findEvenementsAVenirByOrganisationId(eq(ORG_ID), any(Page.class), any(Sort.class)))
                .thenReturn(Collections.emptyList());
        when(cotisationRepository.countByOrganisationId(ORG_ID)).thenReturn(3L);
        // La demande avec org=null est retournée par findByStatut
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(List.of(demandeAvecOrgNull));
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.empty());
        when(membreRepository.countNouveauxMembresByOrganisationId(any(LocalDate.class), eq(ORG_ID))).thenReturn(0L);

        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(BigDecimal.ZERO);

        var stats = dashboardService.getDashboardStats(ORG_ID.toString(), "user@test.com");

        // La demande avec org=null ne contribue pas au pendingRequests pour notre org
        assertThat(stats).isNotNull();
        assertThat(stats.getPendingRequests()).isEqualTo(0);
    }

    @Test
    @DisplayName("getDashboardStats lambda$0:121 — DemandeAide avec organisation différente → filtre retourne false (branche getId().equals() false)")
    void getDashboardStats_demandeAideAutreOrg_filtreRetourneFalse() {
        // DemandeAide EN_ATTENTE avec une organisation différente de ORG_ID → non comptée
        Organisation autreOrg = new Organisation();
        autreOrg.setId(UUID.randomUUID()); // ID différent de ORG_ID

        DemandeAide demandeAutreOrg = new DemandeAide();
        demandeAutreOrg.setId(UUID.randomUUID());
        demandeAutreOrg.setOrganisation(autreOrg); // organisation != null mais ID différent

        when(membreRepository.countDistinctByOrganisationIdIn(any())).thenReturn(0L);
        when(membreRepository.countActifsDistinctByOrganisationIdIn(any())).thenReturn(0L);
        when(evenementRepository.countByOrganisationId(ORG_ID)).thenReturn(0L);
        when(evenementRepository.findEvenementsAVenirByOrganisationId(eq(ORG_ID), any(Page.class), any(Sort.class)))
                .thenReturn(Collections.emptyList());
        when(cotisationRepository.countByOrganisationId(ORG_ID)).thenReturn(0L);
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(List.of(demandeAutreOrg));
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.empty());
        when(membreRepository.countNouveauxMembresByOrganisationId(any(LocalDate.class), eq(ORG_ID))).thenReturn(0L);

        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(BigDecimal.ZERO);

        var stats = dashboardService.getDashboardStats(ORG_ID.toString(), "user@test.com");

        assertThat(stats).isNotNull();
        // La demande de l'autre org n'est pas comptée → 0
        assertThat(stats.getPendingRequests()).isEqualTo(0);
    }

    @Test
    @DisplayName("getDashboardStats lambda$1:145 — org sans typeOrganisation (null) → clé 'Autre' dans la distribution (branche typeOrganisation==null)")
    void getDashboardStats_orgSansType_couvreAutreBrancheMap() {
        // findByIdOptional retourne une org avec typeOrganisation=null → lambda:145 branche "Autre"
        Organisation orgSansType = new Organisation();
        orgSansType.setId(ORG_ID);
        orgSansType.setTypeOrganisation(null); // null → "Autre"

        when(membreRepository.countDistinctByOrganisationIdIn(any())).thenReturn(2L);
        when(membreRepository.countActifsDistinctByOrganisationIdIn(any())).thenReturn(1L);
        when(evenementRepository.countByOrganisationId(ORG_ID)).thenReturn(0L);
        when(evenementRepository.findEvenementsAVenirByOrganisationId(eq(ORG_ID), any(Page.class), any(Sort.class)))
                .thenReturn(Collections.emptyList());
        when(cotisationRepository.countByOrganisationId(ORG_ID)).thenReturn(0L);
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(Collections.emptyList());
        // findByIdOptional retourne l'org avec type null → lambda map() exécutée → "Autre"
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(orgSansType));
        when(membreRepository.countNouveauxMembresByOrganisationId(any(LocalDate.class), eq(ORG_ID))).thenReturn(0L);

        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(BigDecimal.ZERO);

        var stats = dashboardService.getDashboardStats(ORG_ID.toString(), "user@test.com");

        assertThat(stats).isNotNull();
        // typeOrganisation == null → "Autre"
        assertThat(stats.getOrganizationTypeDistribution()).containsKey("Autre");
    }

    // =========================================================================
    // getRecentActivities:218 — loop body with evenement matching orgId → userName = org.getNom()
    // =========================================================================

    @Test
    @DisplayName("getRecentActivities:218 — événement avec org matching orgId entre dans la boucle → userName = org.getNom()")
    void getRecentActivities_evenementMatchingOrgId_loopBodyExecute_L218() {
        // Couvre la branche true de la ternaire L218:
        // evenement.getOrganisation() != null → evenement.getOrganisation().getNom()
        // (la branche false "Système" est structurellement morte car le filtre L207
        //  garantit getOrganisation() != null, mais l'instruction L218 est couverte)
        Organisation org = new Organisation();
        org.setId(ORG_ID);
        org.setNom("Org Evenement Test L218");

        Evenement evenementMatchant = new Evenement();
        evenementMatchant.setId(UUID.randomUUID());
        evenementMatchant.setTitre("Event pour couverture L218");
        evenementMatchant.setOrganisation(org);  // organisation != null && getId().equals(ORG_ID) → passe le filtre L207
        evenementMatchant.setDateCreation(LocalDateTime.now().minusHours(1));

        when(evenementRepository.listAll()).thenReturn(List.of(evenementMatchant));

        List<RecentActivityResponse> activities = dashboardService.getRecentActivities(
                ORG_ID.toString(), "user@test.com", 10);

        // Le loop body est exécuté → activité de type "event" ajoutée
        assertThat(activities).anyMatch(a -> "event".equals(a.getType()));
        // userName = evenement.getOrganisation().getNom() = "Org Evenement Test L218"
        assertThat(activities)
                .filteredOn(a -> "event".equals(a.getType()))
                .extracting(RecentActivityResponse::getUserName)
                .containsExactly("Org Evenement Test L218");
    }

    // =========================================================================
    // L90 : orgIds.isEmpty() = true branch — dead but need to cover via DemandeAide orgId=null path
    // L121 : orgId == null → filter short-circuits to true → DemandeAide comptée (branche orgId==null)
    // =========================================================================

    @Test
    @DisplayName("getDashboardStats avec orgId null → lambda L121 orgId==null → démandeAide toujours comptée")
    void getDashboardStats_orgIdNull_demandeAideToujours_comptee() {
        // orgId = null → filtre d -> orgId==null || ... → true (branche orgId==null true)
        DemandeAide demande = new DemandeAide();
        demande.setId(UUID.randomUUID());
        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());
        demande.setOrganisation(org);

        when(membreRepository.count()).thenReturn(0L);
        when(membreRepository.countActifs()).thenReturn(0L);
        when(evenementRepository.count()).thenReturn(0L);
        when(evenementRepository.findEvenementsAVenir()).thenReturn(Collections.emptyList());
        when(cotisationRepository.count()).thenReturn(0L);
        when(organisationRepository.count()).thenReturn(0L);
        when(organisationRepository.listAll()).thenReturn(Collections.emptyList());
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(List.of(demande));
        when(membreRepository.countNouveauxMembres(any(LocalDate.class))).thenReturn(0L);

        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(BigDecimal.ZERO);

        // orgId = null (organizationId vide) → branche orgId==null dans lambda L121 → true → comptée
        var stats = dashboardService.getDashboardStats("", "user@test.com");

        // La demande est comptée car orgId==null → true
        assertThat(stats.getPendingRequests()).isGreaterThanOrEqualTo(1);
    }

    // =========================================================================
    // L145 : org.getTypeOrganisation() blank (non-null) → branche !isBlank()=false → "Autre"
    // =========================================================================

    @Test
    @DisplayName("getDashboardStats lambda L145 — typeOrganisation blank → 'Autre' (branche isBlank()=true)")
    void getDashboardStats_orgTypeBlank_couvreAutreBranche() {
        Organisation orgTypeBlank = new Organisation();
        orgTypeBlank.setId(ORG_ID);
        orgTypeBlank.setTypeOrganisation("   "); // blank → !isBlank() = false → "Autre"

        when(membreRepository.countDistinctByOrganisationIdIn(any())).thenReturn(0L);
        when(membreRepository.countActifsDistinctByOrganisationIdIn(any())).thenReturn(0L);
        when(evenementRepository.countByOrganisationId(ORG_ID)).thenReturn(0L);
        when(evenementRepository.findEvenementsAVenirByOrganisationId(eq(ORG_ID), any(Page.class), any(Sort.class)))
                .thenReturn(Collections.emptyList());
        when(cotisationRepository.countByOrganisationId(ORG_ID)).thenReturn(0L);
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(Collections.emptyList());
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(orgTypeBlank));
        when(membreRepository.countNouveauxMembresByOrganisationId(any(LocalDate.class), eq(ORG_ID))).thenReturn(0L);

        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(BigDecimal.ZERO);

        var stats = dashboardService.getDashboardStats(ORG_ID.toString(), "user@test.com");

        // typeOrganisation blank → branche isBlank()=true → "Autre"
        assertThat(stats.getOrganizationTypeDistribution()).containsKey("Autre");
    }

    // =========================================================================
    // L186 : membre.getMembresOrganisations() == null → short-circuit false
    // =========================================================================

    @Test
    @DisplayName("getRecentActivities — membre.getMembresOrganisations() null → appartiendAOrg=false (L186)")
    void getRecentActivities_membresOrganisationsNull_appartiendAOrgFalse() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNom("Coulibaly");
        membre.setPrenom("Adama");
        membre.setActif(true);
        membre.setDateCreation(LocalDateTime.now().minusDays(1));
        membre.setMembresOrganisations(null); // null → L186 false → pas d'activité membre

        when(membreRepository.rechercheAvancee(
                isNull(), eq(true), isNull(), isNull(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of(membre));
        when(evenementRepository.listAll()).thenReturn(Collections.emptyList());
        when(cotisationRepository.rechercheAvancee(isNull(), eq("PAYEE"), isNull(), isNull(), isNull(), any(Page.class)))
                .thenReturn(Collections.emptyList());

        List<RecentActivityResponse> activities = dashboardService.getRecentActivities(
                ORG_ID.toString(), "user@test.com", 10);

        // getMembresOrganisations()==null → false → aucune activité membre
        assertThat(activities.stream().noneMatch(a -> "member".equals(a.getType()))).isTrue();
    }

    // =========================================================================
    // L189 : mo.getOrganisation().getId() != orgId → anyMatch retourne false
    // =========================================================================

    @Test
    @DisplayName("getRecentActivities — mo.getOrganisation().getId() != orgId → anyMatch false (L189)")
    void getRecentActivities_moOrgDifferente_anyMatchFalse() {
        Organisation autreOrg = new Organisation();
        autreOrg.setId(UUID.randomUUID()); // ID différent de ORG_ID

        MembreOrganisation mo = new MembreOrganisation();
        mo.setOrganisation(autreOrg); // org != null mais ID différent → L189 getId().equals() = false

        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNom("Touré");
        membre.setPrenom("Bakary");
        membre.setActif(true);
        membre.setDateCreation(LocalDateTime.now().minusDays(1));
        membre.setMembresOrganisations(List.of(mo)); // != null mais anyMatch = false

        when(membreRepository.rechercheAvancee(
                isNull(), eq(true), isNull(), isNull(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of(membre));
        when(evenementRepository.listAll()).thenReturn(Collections.emptyList());
        when(cotisationRepository.rechercheAvancee(isNull(), eq("PAYEE"), isNull(), isNull(), isNull(), any(Page.class)))
                .thenReturn(Collections.emptyList());

        List<RecentActivityResponse> activities = dashboardService.getRecentActivities(
                ORG_ID.toString(), "user@test.com", 10);

        // mo.getOrganisation().getId() != ORG_ID → anyMatch false → pas d'activité membre
        assertThat(activities.stream().noneMatch(a -> "member".equals(a.getType()))).isTrue();
    }

    // =========================================================================
    // L231 : cotisation.getOrganisation() == null → condition false (branche org null)
    // =========================================================================

    @Test
    @DisplayName("getRecentActivities — cotisation.getOrganisation() null → condition L231 false")
    void getRecentActivities_cotisationOrgNull_conditionFalse() {
        Membre membreCotisant = new Membre();
        membreCotisant.setId(UUID.randomUUID());
        membreCotisant.setNom("Diallo");
        membreCotisant.setPrenom("Ousmane");

        Cotisation cotisationSansOrg = new Cotisation();
        cotisationSansOrg.setId(UUID.randomUUID());
        cotisationSansOrg.setMembre(membreCotisant); // membre != null
        cotisationSansOrg.setOrganisation(null);     // org == null → L231 false
        cotisationSansOrg.setMontantPaye(new BigDecimal("5000"));
        cotisationSansOrg.setCodeDevise("XOF");
        cotisationSansOrg.setDateCreation(LocalDateTime.now().minusDays(1));

        when(membreRepository.rechercheAvancee(
                isNull(), eq(true), isNull(), isNull(), any(Page.class), any(Sort.class)))
                .thenReturn(Collections.emptyList());
        when(evenementRepository.listAll()).thenReturn(Collections.emptyList());
        when(cotisationRepository.rechercheAvancee(isNull(), eq("PAYEE"), isNull(), isNull(), isNull(), any(Page.class)))
                .thenReturn(List.of(cotisationSansOrg));

        List<RecentActivityResponse> activities = dashboardService.getRecentActivities(
                ORG_ID.toString(), "user@test.com", 10);

        // org == null → condition L231 = false → pas d'activité contribution
        assertThat(activities.stream().noneMatch(a -> "contribution".equals(a.getType()))).isTrue();
    }

    // =========================================================================
    // L265 : orgId != null, e.getOrganisation() != null, but ID doesn't match → filter false
    // =========================================================================

    @Test
    @DisplayName("getUpcomingEvents lambda L265 — orgId != null, org.id != orgId → filter false")
    void getUpcomingEvents_orgIdNonNull_orgIdDifferent_filtreExclut() {
        Organisation autreOrg = new Organisation();
        autreOrg.setId(UUID.randomUUID()); // ID différent de ORG_ID

        Evenement evenement = new Evenement();
        evenement.setId(UUID.randomUUID());
        evenement.setTitre("Event autre org");
        evenement.setOrganisation(autreOrg); // org != null mais ID != ORG_ID → L265 false
        evenement.setStatut("PLANIFIE");
        evenement.setDateDebut(LocalDateTime.now().plusDays(1));

        when(evenementRepository.findEvenementsAVenirByOrganisationId(
                eq(ORG_ID), any(Page.class), any(Sort.class)))
                .thenReturn(List.of(evenement));

        List<UpcomingEventResponse> events = dashboardService.getUpcomingEvents(
                ORG_ID.toString(), "user@test.com", 5);

        // L265 : orgId != null && e.getOrganisation() != null && getId() != ORG_ID → false → filtré
        assertThat(events).isEmpty();
    }

    @Test
    @DisplayName("calculateMonthlyContributionAmount — organizationId null → requête sans filtre org (branche if false)")
    void calculateMonthlyContributionAmount_orgIdNull_requeteSansFiltreOrg() {
        // Appel via getDashboardStats avec orgId=null → calculateMonthlyHistoricalData(null, 12)
        // → calculateMonthlyContributionAmount(monthStart, monthEnd, null) → branche organizationId==null
        when(membreRepository.count()).thenReturn(5L);
        when(membreRepository.countActifs()).thenReturn(3L);
        when(evenementRepository.count()).thenReturn(0L);
        when(evenementRepository.findEvenementsAVenir()).thenReturn(Collections.emptyList());
        when(cotisationRepository.count()).thenReturn(2L);
        when(organisationRepository.count()).thenReturn(1L);
        when(organisationRepository.listAll()).thenReturn(Collections.emptyList());
        when(demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE)).thenReturn(Collections.emptyList());
        when(membreRepository.countNouveauxMembres(any(LocalDate.class))).thenReturn(0L);

        // Comptes mensuels pour la boucle 12 mois — any(Object[].class) couvre toutes les arités varargs
        when(membreRepository.count(anyString(), any(Object[].class))).thenReturn(5L);
        when(cotisationRepository.count(anyString(), any(Object[].class))).thenReturn(2L);
        when(evenementRepository.count(anyString(), any(Object[].class))).thenReturn(0L);

        EntityManager em = Mockito.mock(EntityManager.class);
        @SuppressWarnings("unchecked")
        TypedQuery<BigDecimal> query = Mockito.mock(TypedQuery.class);
        when(cotisationRepository.getEntityManager()).thenReturn(em);
        // Retourner un query mock valide pour toute requête JPQL
        when(em.createQuery(anyString(), eq(BigDecimal.class))).thenReturn(query);
        when(query.setParameter(anyString(), any())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(BigDecimal.valueOf(50000));

        // organizationId vide → orgId=null → calculateMonthlyContributionAmount(..., null)
        var stats = dashboardService.getDashboardStats("", "user@test.com");

        // Les données historiques mensuelles doivent être présentes (12 mois)
        assertThat(stats.getMonthlyHistoricalData()).hasSize(12);
    }
}
