package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import dev.lions.unionflow.server.api.dto.membre.request.CreateMembreRequest;
import dev.lions.unionflow.server.api.dto.membre.request.UpdateMembreRequest;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
import dev.lions.unionflow.server.api.enums.abonnement.PlageMembres;
import dev.lions.unionflow.server.api.enums.abonnement.StatutSouscription;
import dev.lions.unionflow.server.api.enums.abonnement.TypeFormule;
import dev.lions.unionflow.server.entity.Adresse;
import dev.lions.unionflow.server.entity.FormuleAbonnement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.MembreRole;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.Role;
import dev.lions.unionflow.server.entity.SouscriptionOrganisation;
import dev.lions.unionflow.server.repository.InscriptionEvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.MembreRoleRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectSpy;
import io.quarkus.test.TestTransaction;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import java.security.Principal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@QuarkusTest
class MembreServiceTest {

    @Inject
    MembreService membreService;

    @InjectSpy
    MembreRepository membreRepository;

    @Inject
    EntityManager em;

    @InjectMock
    MembreRoleRepository membreRoleRepository;

    @InjectMock
    TypeReferenceRepository typeReferenceRepository;

    @InjectMock
    MembreImportExportService membreImportExportService;

    @InjectMock
    OrganisationService organisationService;

    @InjectMock
    SecurityIdentity securityIdentity;

    @InjectMock
    InscriptionEvenementRepository inscriptionEvenementRepository;

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

    private Membre membreFixture(String email) {
        Membre m = new Membre();
        m.setId(UUID.randomUUID());
        m.setEmail(email);
        m.setNom("Doe");
        m.setPrenom("John");
        m.setDateNaissance(LocalDate.of(1990, 1, 1));
        m.setNumeroMembre("UF2024-ABCD1234");
        m.setActif(true);
        m.setStatutCompte("ACTIF");
        m.setVersion(0L);
        m.setMembresOrganisations(new ArrayList<>());
        m.setAdresses(new ArrayList<>());
        return m;
    }

    /** By default, securityIdentity behaves as anonymous (no ADMIN_ORGANISATION). */
    @BeforeEach
    void setupDefaultSecurity() {
        Principal principal = () -> "anonymous";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of());
        when(inscriptionEvenementRepository.countByMembre(any())).thenReturn(0L);
    }

    // =========================================================================
    // creerMembre
    // =========================================================================

    @Nested
    @DisplayName("creerMembre")
    class CreerMembreTests {

        @Test
        @DisplayName("Happy path: numéro généré automatiquement, statut EN_ATTENTE_VALIDATION")
        void creerMembre_generatesNumeroAndSetsEnAttenteValidation() {
            Membre membre = new Membre();
            membre.setEmail("test@unionflow.dev");
            membre.setNom("Doe");
            membre.setPrenom("John");
            membre.setDateNaissance(LocalDate.of(1990, 1, 1));

            when(membreRepository.findByEmail("test@unionflow.dev")).thenReturn(Optional.empty());
            when(membreRepository.findByNumeroMembre(any())).thenReturn(Optional.empty());
            doNothing().when(membreRepository).persist(any(Membre.class));

            Membre created = membreService.creerMembre(membre);

            assertThat(created.getNumeroMembre()).startsWith("UF");
            assertThat(created.getStatutCompte()).isEqualTo("EN_ATTENTE_VALIDATION");
            assertThat(created.getActif()).isFalse();
            verify(membreRepository).persist(membre);
        }

        @Test
        @DisplayName("Numéro déjà fourni: conservé tel quel")
        void creerMembre_withExistingNumero_keepsIt() {
            Membre membre = new Membre();
            membre.setEmail("test2@unionflow.dev");
            membre.setNom("Smith");
            membre.setPrenom("Jane");
            membre.setNumeroMembre("UF2024-CUSTOM");
            membre.setDateNaissance(LocalDate.of(1985, 5, 15));

            when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(membreRepository.findByNumeroMembre("UF2024-CUSTOM")).thenReturn(Optional.empty());
            doNothing().when(membreRepository).persist(any(Membre.class));

            Membre created = membreService.creerMembre(membre);

            assertThat(created.getNumeroMembre()).isEqualTo("UF2024-CUSTOM");
        }

        @Test
        @DisplayName("Date de naissance null: valeur par défaut 18 ans")
        void creerMembre_nullDateNaissance_setsDefault() {
            Membre membre = new Membre();
            membre.setEmail("nodate@unionflow.dev");
            membre.setNom("NoDate");
            membre.setPrenom("User");
            // dateNaissance = null intentionally

            when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(membreRepository.findByNumeroMembre(any())).thenReturn(Optional.empty());
            doNothing().when(membreRepository).persist(any(Membre.class));

            Membre created = membreService.creerMembre(membre);

            assertThat(created.getDateNaissance()).isNotNull();
            assertThat(created.getDateNaissance()).isEqualTo(LocalDate.now().minusYears(18));
        }

        @Test
        @DisplayName("Email déjà existant: lève IllegalArgumentException")
        void creerMembre_duplicateEmail_throws() {
            Membre membre = new Membre();
            membre.setEmail("dup@unionflow.dev");
            membre.setNom("Dup");
            membre.setPrenom("User");

            when(membreRepository.findByEmail("dup@unionflow.dev"))
                    .thenReturn(Optional.of(membreFixture("dup@unionflow.dev")));

            assertThatThrownBy(() -> membreService.creerMembre(membre))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("email existe déjà");

            verify(membreRepository, never()).persist(any(Membre.class));
        }

        @Test
        @DisplayName("Numéro de membre déjà existant: lève IllegalArgumentException")
        void creerMembre_duplicateNumero_throws() {
            Membre membre = new Membre();
            membre.setEmail("unique@unionflow.dev");
            membre.setNom("Unique");
            membre.setPrenom("User");
            membre.setNumeroMembre("UF2024-EXIST");

            when(membreRepository.findByEmail("unique@unionflow.dev")).thenReturn(Optional.empty());
            when(membreRepository.findByNumeroMembre("UF2024-EXIST"))
                    .thenReturn(Optional.of(membreFixture("other@unionflow.dev")));

            assertThatThrownBy(() -> membreService.creerMembre(membre))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("numéro existe déjà");

            verify(membreRepository, never()).persist(any(Membre.class));
        }
    }

    // =========================================================================
    // activerMembre
    // =========================================================================

    @Nested
    @DisplayName("activerMembre")
    class ActiverMembreTests {

        @Test
        @DisplayName("Happy path: membre EN_ATTENTE_VALIDATION → ACTIF + actif=true")
        void activerMembre_pendingMember_setsActif() {
            UUID id = UUID.randomUUID();
            Membre membre = membreFixture("pending@unionflow.dev");
            membre.setId(id);
            membre.setStatutCompte("EN_ATTENTE_VALIDATION");
            membre.setActif(false);

            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre));
            doNothing().when(membreRepository).persist(any(Membre.class));

            Membre activated = membreService.activerMembre(id);

            assertThat(activated.getStatutCompte()).isEqualTo("ACTIF");
            assertThat(activated.getActif()).isTrue();
            verify(membreRepository).persist(membre);
        }

        @Test
        @DisplayName("Membre introuvable: lève NotFoundException")
        void activerMembre_notFound_throws() {
            UUID id = UUID.randomUUID();
            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.empty());

            assertThatThrownBy(() -> membreService.activerMembre(id))
                    .isInstanceOf(jakarta.ws.rs.NotFoundException.class)
                    .hasMessageContaining("non trouvé");

            verify(membreRepository, never()).persist(any(Membre.class));
        }
    }

    // =========================================================================
    // promouvoirAdminOrganisation
    // =========================================================================

    @Nested
    @DisplayName("promouvoirAdminOrganisation")
    class PromouvoirAdminOrganisationTests {

        @Test
        @DisplayName("Happy path: membre promu → ACTIF + actif=true immédiatement")
        void promouvoirAdminOrganisation_setsActifImmediately() {
            UUID id = UUID.randomUUID();
            Membre membre = new Membre();
            membre.setId(id);
            membre.setEmail("future-admin@unionflow.dev");
            membre.setNom("Admin");
            membre.setPrenom("New");
            membre.setStatutCompte("EN_ATTENTE_VALIDATION");
            membre.setActif(false);

            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre));
            doNothing().when(membreRepository).persist(any(Membre.class));

            Membre result = membreService.promouvoirAdminOrganisation(id);

            assertThat(result.getStatutCompte()).isEqualTo("ACTIF");
            assertThat(result.getActif()).isTrue();
            verify(membreRepository).persist(membre);
        }

        @Test
        @DisplayName("Membre introuvable: lève NotFoundException")
        void promouvoirAdminOrganisation_notFound_throws() {
            UUID id = UUID.randomUUID();
            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.empty());

            assertThatThrownBy(() -> membreService.promouvoirAdminOrganisation(id))
                    .isInstanceOf(jakarta.ws.rs.NotFoundException.class)
                    .hasMessageContaining("non trouvé");

            verify(membreRepository, never()).persist(any(Membre.class));
        }
    }

    // =========================================================================
    // mettreAJourMembre
    // =========================================================================

    @Nested
    @DisplayName("mettreAJourMembre")
    class MettreAJourMembreTests {

        @Test
        @DisplayName("Membre introuvable: lève IllegalArgumentException")
        void mettreAJourMembre_notFound_throws() {
            UUID id = UUID.randomUUID();
            // UUID aléatoire non présent en H2 → findById retourne null → service throws

            Membre modifie = new Membre();
            modifie.setEmail("any@unionflow.dev");
            modifie.setNom("X");
            modifie.setPrenom("Y");

            assertThatThrownBy(() -> membreService.mettreAJourMembre(id, modifie))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("non trouvé");
        }
    }

    // =========================================================================
    // trouverParId / trouverParEmail
    // =========================================================================

    @Nested
    @DisplayName("trouverParId / trouverParEmail")
    class TrouverTests {

        @Test
        @DisplayName("trouverParId: retourne empty si non trouvé")
        void trouverParId_notFound() {
            UUID id = UUID.randomUUID();
            // UUID aléatoire non présent en H2 → findById retourne null → trouverParId retourne empty

            Optional<Membre> result = membreService.trouverParId(id);

            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("trouverParEmail: délègue au repository")
        void trouverParEmail_delegates() {
            Membre m = membreFixture("byemail@unionflow.dev");
            when(membreRepository.findByEmail("byemail@unionflow.dev")).thenReturn(Optional.of(m));

            Optional<Membre> result = membreService.trouverParEmail("byemail@unionflow.dev");

            assertThat(result).isPresent();
        }
    }

    // =========================================================================
    // listerMembresActifs / rechercherMembres (simple)
    // =========================================================================

    @Nested
    @DisplayName("listerMembresActifs / rechercherMembres simple")
    class ListerTests {

        @Test
        @DisplayName("listerMembresActifs sans pagination: délègue au repository")
        void listerMembresActifs_delegates() {
            List<Membre> membres = List.of(membreFixture("a@test.dev"), membreFixture("b@test.dev"));
            when(membreRepository.findAllActifs()).thenReturn(membres);

            List<Membre> result = membreService.listerMembresActifs();

            assertThat(result).hasSize(2);
        }

        @Test
        @DisplayName("listerMembresActifs avec pagination: délègue au repository")
        void listerMembresActifs_paged() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();
            List<Membre> membres = List.of(membreFixture("c@test.dev"));
            when(membreRepository.findAllActifs(page, sort)).thenReturn(membres);

            List<Membre> result = membreService.listerMembresActifs(page, sort);

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("rechercherMembres sans pagination: délègue au repository")
        void rechercherMembres_simple() {
            List<Membre> membres = List.of(membreFixture("search@test.dev"));
            when(membreRepository.findByNomOrPrenom("doe")).thenReturn(membres);

            List<Membre> result = membreService.rechercherMembres("doe");

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("compterMembresActifs: délègue au repository")
        void compterMembresActifs() {
            when(membreRepository.countActifs()).thenReturn(42L);

            long count = membreService.compterMembresActifs();

            assertThat(count).isEqualTo(42L);
        }
    }

    // =========================================================================
    // desactiverMembre
    // =========================================================================

    @Nested
    @DisplayName("desactiverMembre")
    class DesactiverTests {

        @Test
        @DisplayName("Membre introuvable: lève IllegalArgumentException")
        void desactiverMembre_notFound_throws() {
            UUID id = UUID.randomUUID();
            // UUID aléatoire non présent en H2 → findById retourne null → service throws

            assertThatThrownBy(() -> membreService.desactiverMembre(id))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("non trouvé");
        }
    }

    // =========================================================================
    // listerMembres (avec logique ADMIN_ORGANISATION)
    // =========================================================================

    @Nested
    @DisplayName("listerMembres (avec logique ADMIN_ORGANISATION)")
    class ListerMembresSecurityTests {

        @Test
        @DisplayName("Utilisateur sans rôle ADMIN_ORGANISATION: liste tous les membres")
        void listerMembres_noAdminOrgRole_returnsAll() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();
            List<Membre> all = List.of(membreFixture("x@test.dev"));

            when(securityIdentity.getRoles()).thenReturn(Set.of("USER"));
            doReturn(all).when(membreRepository).findAll(page, sort);

            List<Membre> result = membreService.listerMembres(page, sort);

            assertThat(result).hasSize(1);
            verify(membreRepository).findAll(page, sort);
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION avec organisations: filtre par organisations")
        void listerMembres_adminOrg_filtersToOrgs() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();

            UUID orgId = UUID.randomUUID();
            Organisation org = new Organisation();
            org.setId(orgId);
            org.setNom("Test Org");

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of(org));

            List<Membre> orgMembres = List.of(membreFixture("m@test.dev"));
            when(membreRepository.findDistinctByOrganisationIdIn(any(), eq(page), eq(sort)))
                    .thenReturn(orgMembres);

            List<Membre> result = membreService.listerMembres(page, sort);

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION sans organisations: retourne liste vide")
        void listerMembres_adminOrg_noOrgs_returnsEmpty() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of());

            List<Membre> result = membreService.listerMembres(page, sort);

            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION + ADMIN: pas de restriction (retourne tous)")
        void listerMembres_adminOrgAndAdmin_returnsAll() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();
            List<Membre> all = List.of(membreFixture("y@test.dev"));

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION", "ADMIN"));
            when(membreRepository.findAll(page, sort)).thenReturn(all);

            List<Membre> result = membreService.listerMembres(page, sort);

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("SecurityIdentity null (principal null): retourne tous les membres")
        void listerMembres_nullPrincipal_returnsAll() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();
            List<Membre> all = List.of(membreFixture("z@test.dev"));

            when(securityIdentity.getPrincipal()).thenReturn(null);
            when(membreRepository.findAll(page, sort)).thenReturn(all);

            List<Membre> result = membreService.listerMembres(page, sort);

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("Roles null: retourne tous les membres")
        void listerMembres_nullRoles_returnsAll() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();
            List<Membre> all = List.of(membreFixture("z2@test.dev"));

            Principal principal = () -> "user@test.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(null);
            when(membreRepository.findAll(page, sort)).thenReturn(all);

            List<Membre> result = membreService.listerMembres(page, sort);

            assertThat(result).hasSize(1);
        }
    }

    // =========================================================================
    // compterMembres (avec logique ADMIN_ORGANISATION)
    // =========================================================================

    @Nested
    @DisplayName("compterMembres")
    class CompterMembresTests {

        @Test
        @DisplayName("Utilisateur normal: compte tous les membres")
        void compterMembres_noAdminOrg_countsAll() {
            when(securityIdentity.getRoles()).thenReturn(Set.of());
            doReturn(100L).when(membreRepository).count();

            long count = membreService.compterMembres();

            assertThat(count).isEqualTo(100L);
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION avec organisations: compte par org")
        void compterMembres_adminOrg_countsFiltered() {
            UUID orgId = UUID.randomUUID();
            Organisation org = new Organisation();
            org.setId(orgId);

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of(org));
            when(membreRepository.countDistinctByOrganisationIdIn(any())).thenReturn(15L);

            long count = membreService.compterMembres();

            assertThat(count).isEqualTo(15L);
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION sans organisations: retourne 0")
        void compterMembres_adminOrg_noOrgs_returnsZero() {
            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of());

            long count = membreService.compterMembres();

            assertThat(count).isEqualTo(0L);
        }
    }

    // =========================================================================
    // rechercherMembres avec pagination
    // =========================================================================

    @Nested
    @DisplayName("rechercherMembres (avec pagination)")
    class RechercherMembresPagedTests {

        @Test
        @DisplayName("Utilisateur normal: recherche tous")
        void rechercherMembres_paged_noAdminOrg() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();
            List<Membre> membres = List.of(membreFixture("r@test.dev"));
            when(securityIdentity.getRoles()).thenReturn(Set.of());
            when(membreRepository.findByNomOrPrenom("doe", page, sort)).thenReturn(membres);

            List<Membre> result = membreService.rechercherMembres("doe", page, sort);

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION avec organisations: filtre")
        void rechercherMembres_paged_adminOrg() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();

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

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of(org));
            when(membreRepository.findByNomOrPrenomAndOrganisationIdIn(eq("doe"), any(), eq(page), eq(sort)))
                    .thenReturn(List.of(membreFixture("r@test.dev")));

            List<Membre> result = membreService.rechercherMembres("doe", page, sort);

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION sans organisations: retourne liste vide")
        void rechercherMembres_paged_adminOrg_noOrgs() {
            Page page = Page.of(0, 10);
            Sort sort = Sort.by("nom").ascending();

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of());

            List<Membre> result = membreService.rechercherMembres("doe", page, sort);

            assertThat(result).isEmpty();
        }
    }

    // =========================================================================
    // obtenirStatistiquesAvancees
    // =========================================================================

    @Test
    @DisplayName("obtenirStatistiquesAvancees: calcule le taux d'activité correctement")
    void obtenirStatistiquesAvancees_calculatesCorrectly() {
        doReturn(100L).when(membreRepository).count();
        doReturn(80L).when(membreRepository).countActifs();
        doReturn(10L).when(membreRepository).countNouveauxMembres(any());
        doReturn(5L).when(organisationService).rechercherOrganisationsCount("");

        Map<String, Object> stats = membreService.obtenirStatistiquesAvancees();

        assertThat(stats).containsKey("totalMembres");
        assertThat(stats).containsKey("total");            // alias mobile
        assertThat(stats).containsKey("totalOrganisations");
        assertThat(stats).containsKey("membresActifs");
        assertThat(stats).containsKey("membresInactifs");
        assertThat(stats).containsKey("tauxActivite");
        assertThat(stats.get("totalMembres")).isEqualTo(100L);
        assertThat(stats.get("total")).isEqualTo(100L);
        assertThat(stats.get("totalOrganisations")).isEqualTo(5L);
        assertThat(stats.get("membresActifs")).isEqualTo(80L);
        assertThat(stats.get("membresInactifs")).isEqualTo(20L);
        assertThat((Double) stats.get("tauxActivite")).isEqualTo(80.0);
    }

    @Test
    @DisplayName("obtenirStatistiquesAvancees: taux 0 si pas de membres")
    void obtenirStatistiquesAvancees_noMembers_tauxZero() {
        doReturn(0L).when(membreRepository).count();
        doReturn(0L).when(membreRepository).countActifs();
        doReturn(0L).when(membreRepository).countNouveauxMembres(any());
        doReturn(0L).when(organisationService).rechercherOrganisationsCount("");

        Map<String, Object> stats = membreService.obtenirStatistiquesAvancees();

        assertThat((Double) stats.get("tauxActivite")).isEqualTo(0.0);
        assertThat(stats.get("totalOrganisations")).isEqualTo(0L);
    }

    // =========================================================================
    // convertToResponse
    // =========================================================================

    @Nested
    @DisplayName("convertToResponse")
    class ConvertToResponseTests {

        @Test
        @DisplayName("null retourne null")
        void convertToResponse_null() {
            assertThat(membreService.convertToResponse(null)).isNull();
        }

        @Test
        @DisplayName("Membre minimal sans organisation ni rôles")
        void convertToResponse_minimalMembre() {
            Membre m = membreFixture("resp@test.dev");
            m.setStatutCompte(null);
            m.setStatutMatrimonial(null);
            m.setTypeIdentite(null);
            m.setMembresOrganisations(new ArrayList<>());

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp).isNotNull();
            assertThat(resp.getEmail()).isEqualTo("resp@test.dev");
            assertThat(resp.getRoles()).isEmpty();
            assertThat(resp.getOrganisationId()).isNull();
        }

        @Test
        @DisplayName("Membre avec statut matrimonial et typeIdentite: résout les libellés")
        void convertToResponse_withReferenceData() {
            Membre m = membreFixture("full@test.dev");
            m.setStatutMatrimonial("MARIE");
            m.setTypeIdentite("CNI");
            m.setStatutCompte("ACTIF");
            m.setMembresOrganisations(new ArrayList<>());
            m.setVersion(2L);

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());
            when(typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_MATRIMONIAL", "MARIE"))
                    .thenReturn("Marié(e)");
            when(typeReferenceRepository.findLibelleByDomaineAndCode("TYPE_IDENTITE", "CNI"))
                    .thenReturn("Carte Nationale d'Identité");
            when(typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_COMPTE", "ACTIF"))
                    .thenReturn("Actif");
            when(typeReferenceRepository.findSeverityByDomaineAndCode("STATUT_COMPTE", "ACTIF"))
                    .thenReturn("success");

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getStatutMatrimonialLibelle()).isEqualTo("Marié(e)");
            assertThat(resp.getTypeIdentiteLibelle()).isEqualTo("Carte Nationale d'Identité");
            assertThat(resp.getStatutCompteLibelle()).isEqualTo("Actif");
            assertThat(resp.getStatutCompteSeverity()).isEqualTo("success");
            assertThat(resp.getVersion()).isEqualTo(2L);
        }

        @Test
        @DisplayName("Membre avec rôles actifs: les codes sont inclus")
        void convertToResponse_withRoles() {
            Membre m = membreFixture("roles@test.dev");
            m.setMembresOrganisations(new ArrayList<>());

            Role role = new Role();
            role.setCode("PRESIDENT");

            MembreRole mr = new MembreRole();
            mr.setRole(role);

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of(mr));

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getRoles()).contains("PRESIDENT");
        }

        @Test
        @DisplayName("Membre avec rôle dont role est null: filtré")
        void convertToResponse_roleNullFiltered() {
            Membre m = membreFixture("nullrole@test.dev");
            m.setMembresOrganisations(new ArrayList<>());

            MembreRole mr = new MembreRole();
            mr.setRole(null); // null role

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of(mr));

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getRoles()).isEmpty();
        }

        @Test
        @DisplayName("Membre avec organisation: organisationId et nom remplis")
        void convertToResponse_withOrganisation() {
            UUID orgId = UUID.randomUUID();
            Organisation org = new Organisation();
            org.setId(orgId);
            org.setNom("Lions Club");

            MembreOrganisation mo = new MembreOrganisation();
            mo.setOrganisation(org);
            mo.setDateAdhesion(LocalDate.of(2022, 6, 1));
            mo.setMembre(new Membre());

            Membre m = membreFixture("org@test.dev");
            m.setMembresOrganisations(new ArrayList<>(List.of(mo)));

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getOrganisationId()).isEqualTo(orgId);
            assertThat(resp.getOrganisationNom()).isEqualTo("Lions Club");
            assertThat(resp.getDateAdhesion()).isEqualTo(LocalDate.of(2022, 6, 1));
        }

        @Test
        @DisplayName("Membre avec MembreOrganisation dont organisation est null: pas d'exception")
        void convertToResponse_moWithNullOrg() {
            MembreOrganisation mo = new MembreOrganisation();
            mo.setOrganisation(null);
            mo.setMembre(new Membre());

            Membre m = membreFixture("nullorg@test.dev");
            m.setMembresOrganisations(new ArrayList<>(List.of(mo)));

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getOrganisationId()).isNull();
            assertThat(resp.getOrganisationNom()).isNull();
        }

        @Test
        @DisplayName("Version null: retourne 0")
        void convertToResponse_versionNull() {
            Membre m = membreFixture("ver@test.dev");
            m.setVersion(null);
            m.setMembresOrganisations(new ArrayList<>());
            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getVersion()).isEqualTo(0L);
        }

        @Test
        @DisplayName("Adresse principale: adresse, ville, codePostal remplis")
        void convertToResponse_withPrincipalAdresse() {
            Membre m = membreFixture("addr@test.dev");
            m.setMembresOrganisations(new ArrayList<>());

            Adresse a = new Adresse();
            a.setActif(true);
            a.setPrincipale(true);
            a.setAdresse("12 Rue des Lions");
            a.setVille("Abidjan");
            a.setCodePostal("01 BP 123");
            m.setAdresses(new ArrayList<>(List.of(a)));

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getAdresse()).isEqualTo("12 Rue des Lions");
            assertThat(resp.getVille()).isEqualTo("Abidjan");
            assertThat(resp.getCodePostal()).isEqualTo("01 BP 123");
        }

        @Test
        @DisplayName("Adresse non principale active: utilisée en fallback")
        void convertToResponse_withNonPrincipalActifAdresse() {
            Membre m = membreFixture("addr2@test.dev");
            m.setMembresOrganisations(new ArrayList<>());

            Adresse a = new Adresse();
            a.setActif(true);
            a.setPrincipale(false);
            a.setAdresse("5 Avenue de la Paix");
            a.setVille("Bouaké");
            a.setCodePostal(null);
            m.setAdresses(new ArrayList<>(List.of(a)));

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getAdresse()).isEqualTo("5 Avenue de la Paix");
            assertThat(resp.getVille()).isEqualTo("Bouaké");
            assertThat(resp.getCodePostal()).isNull();
        }

        @Test
        @DisplayName("Adresse inactive uniquement: pas de champs adresse")
        void convertToResponse_withInactiveAdresseOnly() {
            Membre m = membreFixture("addr3@test.dev");
            m.setMembresOrganisations(new ArrayList<>());

            Adresse a = new Adresse();
            a.setActif(false);
            a.setPrincipale(true);
            a.setAdresse("Adresse inactive");
            a.setVille("Yamoussoukro");
            m.setAdresses(new ArrayList<>(List.of(a)));

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getAdresse()).isNull();
            assertThat(resp.getVille()).isNull();
        }

        @Test
        @DisplayName("Notes remplies: propagées dans la réponse")
        void convertToResponse_withNotes() {
            Membre m = membreFixture("notes@test.dev");
            m.setMembresOrganisations(new ArrayList<>());
            m.setNotes("Biographie du membre");

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getNotes()).isEqualTo("Biographie du membre");
        }

        @Test
        @DisplayName("Pas d'organisation et dateCreation non nulle: dateAdhesion fallback")
        void convertToResponse_noOrg_dateCreationFallback() {
            Membre m = membreFixture("fallback@test.dev");
            m.setMembresOrganisations(new ArrayList<>());
            java.time.LocalDateTime dateCreation = java.time.LocalDateTime.of(2025, 6, 15, 10, 0, 0);
            m.setDateCreation(dateCreation);

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getDateAdhesion()).isEqualTo(LocalDate.of(2025, 6, 15));
        }

        @Test
        @DisplayName("nombreEvenementsParticipes: propagé depuis le repository")
        void convertToResponse_nombreEvenementsParticipes() {
            Membre m = membreFixture("events@test.dev");
            m.setMembresOrganisations(new ArrayList<>());
            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());
            when(inscriptionEvenementRepository.countByMembre(m.getId())).thenReturn(3L);

            MembreResponse resp = membreService.convertToResponse(m);

            assertThat(resp.getNombreEvenementsParticipes()).isEqualTo(3);
        }
    }

    // =========================================================================
    // convertToSummaryResponse
    // =========================================================================

    @Nested
    @DisplayName("convertToSummaryResponse")
    class ConvertToSummaryResponseTests {

        @Test
        @DisplayName("null retourne null")
        void convertToSummaryResponse_null() {
            assertThat(membreService.convertToSummaryResponse(null)).isNull();
        }

        @Test
        @DisplayName("Membre sans rôles ni statut: rôles vides, libellés null")
        void convertToSummaryResponse_noRolesNoStatut() {
            Membre m = membreFixture("sum@test.dev");
            m.setStatutCompte(null);
            m.setMembresOrganisations(new ArrayList<>());

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreSummaryResponse resp = membreService.convertToSummaryResponse(m);

            assertThat(resp).isNotNull();
            assertThat(resp.getRoles()).isEmpty();
            assertThat(resp.getStatutCompteLibelle()).isNull();
            assertThat(resp.getStatutCompteSeverity()).isNull();
        }

        @Test
        @DisplayName("Membre avec statut ACTIF: libellé et severity résolus")
        void convertToSummaryResponse_withStatut() {
            Membre m = membreFixture("statut@test.dev");
            m.setStatutCompte("ACTIF");
            m.setMembresOrganisations(new ArrayList<>());

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());
            when(typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_COMPTE", "ACTIF"))
                    .thenReturn("Actif");
            when(typeReferenceRepository.findSeverityByDomaineAndCode("STATUT_COMPTE", "ACTIF"))
                    .thenReturn("success");

            MembreSummaryResponse resp = membreService.convertToSummaryResponse(m);

            assertThat(resp.getStatutCompteLibelle()).isEqualTo("Actif");
            assertThat(resp.getStatutCompteSeverity()).isEqualTo("success");
        }

        @Test
        @DisplayName("Membre avec rôles actifs: codes inclus")
        void convertToSummaryResponse_withRoles() {
            Membre m = membreFixture("sumroles@test.dev");
            m.setMembresOrganisations(new ArrayList<>());

            Role role = new Role();
            role.setCode("TRESORIER");
            MembreRole mr = new MembreRole();
            mr.setRole(role);

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of(mr));

            MembreSummaryResponse resp = membreService.convertToSummaryResponse(m);

            assertThat(resp.getRoles()).contains("TRESORIER");
        }

        @Test
        @DisplayName("Membre avec organisation: organisationId rempli")
        void convertToSummaryResponse_withOrganisation() {
            UUID orgId = UUID.randomUUID();
            Organisation org = new Organisation();
            org.setId(orgId);
            org.setNom("Assoc Test");

            MembreOrganisation mo = new MembreOrganisation();
            mo.setOrganisation(org);
            mo.setMembre(new Membre());

            Membre m = membreFixture("sumorg@test.dev");
            m.setMembresOrganisations(new ArrayList<>(List.of(mo)));

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreSummaryResponse resp = membreService.convertToSummaryResponse(m);

            assertThat(resp.getOrganisationId()).isEqualTo(orgId);
            assertThat(resp.getOrganisationNom()).isEqualTo("Assoc Test");
        }

        @Test
        @DisplayName("MembreOrganisation avec organisation null: pas d'exception")
        void convertToSummaryResponse_moWithNullOrg() {
            MembreOrganisation mo = new MembreOrganisation();
            mo.setOrganisation(null);
            mo.setMembre(new Membre());

            Membre m = membreFixture("nullorgsum@test.dev");
            m.setMembresOrganisations(new ArrayList<>(List.of(mo)));

            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            MembreSummaryResponse resp = membreService.convertToSummaryResponse(m);

            assertThat(resp.getOrganisationId()).isNull();
        }
    }

    // =========================================================================
    // convertFromCreateRequest
    // =========================================================================

    @Nested
    @DisplayName("convertFromCreateRequest")
    class ConvertFromCreateRequestTests {

        @Test
        @DisplayName("null retourne null")
        void convertFromCreateRequest_null() {
            assertThat(membreService.convertFromCreateRequest(null)).isNull();
        }

        @Test
        @DisplayName("DTO complet: tous les champs copiés")
        void convertFromCreateRequest_fullDto() {
            LocalDate dateNaissance = LocalDate.of(1990, 5, 15);
            CreateMembreRequest req = CreateMembreRequest.builder()
                    .nom("Diallo")
                    .prenom("Aminata")
                    .email("aminata@test.dev")
                    .telephone("0600000001")
                    .telephoneWave("+2250700000001")
                    .dateNaissance(dateNaissance)
                    .profession("Ingénieure")
                    .photoUrl("https://example.com/photo.jpg")
                    .statutMatrimonial("CELIBATAIRE")
                    .nationalite("Ivoirienne")
                    .typeIdentite("CNI")
                    .numeroIdentite("ABC123456")
                    .organisationId(UUID.randomUUID())
                    .build();

            Membre membre = membreService.convertFromCreateRequest(req);

            assertThat(membre.getNom()).isEqualTo("Diallo");
            assertThat(membre.getPrenom()).isEqualTo("Aminata");
            assertThat(membre.getEmail()).isEqualTo("aminata@test.dev");
            assertThat(membre.getTelephone()).isEqualTo("0600000001");
            assertThat(membre.getTelephoneWave()).isEqualTo("+2250700000001");
            assertThat(membre.getDateNaissance()).isEqualTo(dateNaissance);
            assertThat(membre.getProfession()).isEqualTo("Ingénieure");
            assertThat(membre.getPhotoUrl()).isEqualTo("https://example.com/photo.jpg");
            assertThat(membre.getStatutMatrimonial()).isEqualTo("CELIBATAIRE");
            assertThat(membre.getNationalite()).isEqualTo("Ivoirienne");
            assertThat(membre.getTypeIdentite()).isEqualTo("CNI");
            assertThat(membre.getNumeroIdentite()).isEqualTo("ABC123456");
        }
    }

    // =========================================================================
    // updateFromRequest
    // =========================================================================

    @Nested
    @DisplayName("updateFromRequest")
    class UpdateFromRequestTests {

        @Test
        @DisplayName("null membre ou null dto: aucune mise à jour")
        void updateFromRequest_nullInputs_noOp() {
            Membre m = membreFixture("before@test.dev");
            // Both null
            membreService.updateFromRequest(null, null);
            // null dto
            membreService.updateFromRequest(m, null);
            // null membre
            UpdateMembreRequest req = UpdateMembreRequest.builder()
                    .nom("X")
                    .prenom("Y")
                    .email("z@test.dev")
                    .dateNaissance(LocalDate.now())
                    .build();
            membreService.updateFromRequest(null, req);

            // email remains unchanged on original membre
            assertThat(m.getEmail()).isEqualTo("before@test.dev");
        }

        @Test
        @DisplayName("DTO complet avec actif non null: tous les champs mis à jour")
        void updateFromRequest_fullDto_allUpdated() {
            Membre m = membreFixture("old@test.dev");
            LocalDate newDate = LocalDate.of(1995, 8, 20);

            UpdateMembreRequest req = UpdateMembreRequest.builder()
                    .nom("NouveauNom")
                    .prenom("NouveauPrenom")
                    .email("new@test.dev")
                    .telephone("0700000002")
                    .telephoneWave("+2250700000002")
                    .dateNaissance(newDate)
                    .profession("Médecin")
                    .photoUrl("https://example.com/new.jpg")
                    .statutMatrimonial("MARIE")
                    .nationalite("Sénégalaise")
                    .typeIdentite("PASSEPORT")
                    .numeroIdentite("PA123456")
                    .actif(false)
                    .build();

            membreService.updateFromRequest(m, req);

            assertThat(m.getNom()).isEqualTo("NouveauNom");
            assertThat(m.getPrenom()).isEqualTo("NouveauPrenom");
            assertThat(m.getEmail()).isEqualTo("new@test.dev");
            assertThat(m.getTelephone()).isEqualTo("0700000002");
            assertThat(m.getDateNaissance()).isEqualTo(newDate);
            assertThat(m.getProfession()).isEqualTo("Médecin");
            assertThat(m.getActif()).isFalse();
            assertThat(m.getDateModification()).isNotNull();
        }

        @Test
        @DisplayName("actif null dans le DTO: flag actif non modifié")
        void updateFromRequest_actifNull_notModified() {
            Membre m = membreFixture("keep@test.dev");
            m.setActif(true);

            UpdateMembreRequest req = UpdateMembreRequest.builder()
                    .nom("KeepActif")
                    .prenom("Test")
                    .email("keep@test.dev")
                    .dateNaissance(LocalDate.now())
                    .actif(null) // null → should not change actif
                    .build();

            membreService.updateFromRequest(m, req);

            assertThat(m.getActif()).isTrue();
        }
    }

    // =========================================================================
    // convertToResponseList / convertToSummaryResponseList
    // =========================================================================

    @Nested
    @DisplayName("convertToResponseList / convertToSummaryResponseList")
    class ConvertListTests {

        @Test
        @DisplayName("liste null: retourne liste vide")
        void convertToResponseList_null_returnsEmpty() {
            List<MembreResponse> result = membreService.convertToResponseList(null);
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("liste vide: retourne liste vide")
        void convertToResponseList_empty_returnsEmpty() {
            List<MembreResponse> result = membreService.convertToResponseList(new ArrayList<>());
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("liste de 2 membres: retourne 2 réponses")
        void convertToResponseList_twoMembres() {
            Membre m1 = membreFixture("a@test.dev");
            Membre m2 = membreFixture("b@test.dev");
            when(membreRoleRepository.findActifsByMembreId(m1.getId())).thenReturn(List.of());
            when(membreRoleRepository.findActifsByMembreId(m2.getId())).thenReturn(List.of());

            List<MembreResponse> result = membreService.convertToResponseList(List.of(m1, m2));

            assertThat(result).hasSize(2);
        }

        @Test
        @DisplayName("convertToSummaryResponseList null: retourne liste vide")
        void convertToSummaryResponseList_null() {
            List<MembreSummaryResponse> result = membreService.convertToSummaryResponseList(null);
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("convertToSummaryResponseList de 1 membre")
        void convertToSummaryResponseList_oneMembre() {
            Membre m = membreFixture("s@test.dev");
            m.setMembresOrganisations(new ArrayList<>());
            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            List<MembreSummaryResponse> result = membreService.convertToSummaryResponseList(List.of(m));

            assertThat(result).hasSize(1);
        }
    }

    // =========================================================================
    // rechercheAvancee (deprecated)
    // =========================================================================

    @Test
    @DisplayName("rechercheAvancee (deprecated): délègue au repository")
    void rechercheAvancee_deprecated_delegates() {
        Page page = Page.of(0, 10);
        Sort sort = Sort.by("nom").ascending();
        List<Membre> membres = List.of(membreFixture("adv@test.dev"));
        LocalDate min = LocalDate.of(2020, 1, 1);
        LocalDate max = LocalDate.of(2024, 12, 31);

        when(membreRepository.rechercheAvancee("john", true, min, max, page, sort))
                .thenReturn(membres);

        List<Membre> result = membreService.rechercheAvancee("john", true, min, max, page, sort);

        assertThat(result).hasSize(1);
    }

    // =========================================================================
    // exporterMembresSelectionnes
    // =========================================================================

    @Nested
    @DisplayName("exporterMembresSelectionnes")
    class ExporterMembresTests {

        @Test
        @DisplayName("liste null: lève IllegalArgumentException")
        void exporterMembresSelectionnes_null_throws() {
            assertThatThrownBy(() -> membreService.exporterMembresSelectionnes(null, "CSV"))
                    .isInstanceOf(IllegalArgumentException.class);
        }

        @Test
        @DisplayName("liste vide: lève IllegalArgumentException")
        void exporterMembresSelectionnes_empty_throws() {
            assertThatThrownBy(() -> membreService.exporterMembresSelectionnes(List.of(), "CSV"))
                    .isInstanceOf(IllegalArgumentException.class);
        }

        @Test
        @DisplayName("membres non trouvés: retourne CSV avec en-tête seulement")
        void exporterMembresSelectionnes_notFound_returnsHeaderOnlyCsv() {
            UUID id = UUID.randomUUID();
            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.empty());

            byte[] result = membreService.exporterMembresSelectionnes(List.of(id), "CSV");

            assertThat(result).isNotNull();
            String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8);
            assertThat(csv).contains("Numéro");
        }

        @Test
        @DisplayName("membres trouvés: retourne CSV avec données")
        void exporterMembresSelectionnes_found_returnsCsvWithData() {
            UUID id = UUID.randomUUID();
            Membre m = membreFixture("exp@test.dev");
            m.setId(id);
            m.setMembresOrganisations(new ArrayList<>());
            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(m));
            when(membreRoleRepository.findActifsByMembreId(id)).thenReturn(List.of());

            byte[] result = membreService.exporterMembresSelectionnes(List.of(id), "CSV");

            assertThat(result).isNotNull();
            String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8);
            assertThat(csv).contains("exp@test.dev");
        }
    }

    // =========================================================================
    // exporterVersExcel / exporterVersCSV / exporterVersPDF / genererModeleImport
    // =========================================================================

    @Nested
    @DisplayName("Export/Import delegation tests")
    class ExportImportDelegationTests {

        @Test
        @DisplayName("exporterVersExcel: délègue et retourne résultat")
        void exporterVersExcel_delegates() throws Exception {
            byte[] data = "excel".getBytes();
            when(membreImportExportService.exporterVersExcel(any(), any(), eq(true), eq(true), eq(false), any()))
                    .thenReturn(data);

            byte[] result = membreService.exporterVersExcel(List.of(), List.of(), true, true, false, null);

            assertThat(result).isEqualTo(data);
        }

        @Test
        @DisplayName("exporterVersExcel: exception propagée comme RuntimeException")
        void exporterVersExcel_exception_wraps() throws Exception {
            when(membreImportExportService.exporterVersExcel(any(), any(), anyBoolean(), anyBoolean(), anyBoolean(), any()))
                    .thenThrow(new RuntimeException("Excel error"));

            assertThatThrownBy(() -> membreService.exporterVersExcel(List.of(), List.of(), true, true, false, null))
                    .isInstanceOf(RuntimeException.class)
                    .hasMessageContaining("Excel");
        }

        @Test
        @DisplayName("exporterVersCSV: délègue et retourne résultat")
        void exporterVersCSV_delegates() throws Exception {
            byte[] data = "csv".getBytes();
            when(membreImportExportService.exporterVersCSV(any(), any(), eq(true), eq(false)))
                    .thenReturn(data);

            byte[] result = membreService.exporterVersCSV(List.of(), List.of(), true, false);

            assertThat(result).isEqualTo(data);
        }

        @Test
        @DisplayName("exporterVersCSV: exception propagée comme RuntimeException")
        void exporterVersCSV_exception_wraps() throws Exception {
            when(membreImportExportService.exporterVersCSV(any(), any(), anyBoolean(), anyBoolean()))
                    .thenThrow(new RuntimeException("CSV error"));

            assertThatThrownBy(() -> membreService.exporterVersCSV(List.of(), List.of(), true, false))
                    .isInstanceOf(RuntimeException.class)
                    .hasMessageContaining("CSV");
        }

        @Test
        @DisplayName("exporterVersPDF: délègue et retourne résultat")
        void exporterVersPDF_delegates() throws Exception {
            byte[] data = "pdf".getBytes();
            when(membreImportExportService.exporterVersPDF(any(), any(), eq(true), eq(false), eq(true)))
                    .thenReturn(data);

            byte[] result = membreService.exporterVersPDF(List.of(), List.of(), true, false, true);

            assertThat(result).isEqualTo(data);
        }

        @Test
        @DisplayName("exporterVersPDF: exception propagée comme RuntimeException")
        void exporterVersPDF_exception_wraps() throws Exception {
            when(membreImportExportService.exporterVersPDF(any(), any(), anyBoolean(), anyBoolean(), anyBoolean()))
                    .thenThrow(new RuntimeException("PDF error"));

            assertThatThrownBy(() -> membreService.exporterVersPDF(List.of(), List.of(), true, false, true))
                    .isInstanceOf(RuntimeException.class)
                    .hasMessageContaining("PDF");
        }

        @Test
        @DisplayName("genererModeleImport: délègue et retourne résultat")
        void genererModeleImport_delegates() throws Exception {
            byte[] data = "template".getBytes();
            when(membreImportExportService.genererModeleImport()).thenReturn(data);

            byte[] result = membreService.genererModeleImport();

            assertThat(result).isEqualTo(data);
        }

        @Test
        @DisplayName("genererModeleImport: exception propagée comme RuntimeException")
        void genererModeleImport_exception_wraps() throws Exception {
            when(membreImportExportService.genererModeleImport()).thenThrow(new RuntimeException("Template error"));

            assertThatThrownBy(() -> membreService.genererModeleImport())
                    .isInstanceOf(RuntimeException.class)
                    .hasMessageContaining("modèle");
        }
    }

    // =========================================================================
    // listerMembresParOrganisations (uses EntityManager — excluded from mock tests)
    // =========================================================================

    @Test
    @DisplayName("listerMembresParOrganisations: liste null retourne liste vide")
    void listerMembresParOrganisations_null_returnsEmpty() {
        // This path doesn't touch EntityManager
        List<Membre> result = membreService.listerMembresParOrganisations(null, Page.of(0, 10), Sort.by("nom"));
        assertThat(result).isEmpty();
    }

    @Test
    @DisplayName("listerMembresParOrganisations: liste vide retourne liste vide")
    void listerMembresParOrganisations_empty_returnsEmpty() {
        List<Membre> result = membreService.listerMembresParOrganisations(List.of(), Page.of(0, 10), Sort.by("nom"));
        assertThat(result).isEmpty();
    }

    // =========================================================================
    // obtenirVillesDistinctes / obtenirProfessionsDistinctes
    // (use EntityManager directly — available in @QuarkusTest context)
    // =========================================================================

    @Test
    @DisplayName("obtenirVillesDistinctes sans query: retourne une liste (peut être vide)")
    void obtenirVillesDistinctes_noQuery_returnsList() {
        List<String> villes = membreService.obtenirVillesDistinctes(null);
        assertThat(villes).isNotNull();
    }

    @Test
    @DisplayName("obtenirVillesDistinctes avec query vide: retourne une liste")
    void obtenirVillesDistinctes_emptyQuery_returnsList() {
        List<String> villes = membreService.obtenirVillesDistinctes("");
        assertThat(villes).isNotNull();
    }

    @Test
    @DisplayName("obtenirVillesDistinctes avec query: retourne une liste filtrée")
    void obtenirVillesDistinctes_withQuery_returnsList() {
        List<String> villes = membreService.obtenirVillesDistinctes("Paris");
        assertThat(villes).isNotNull();
    }

    @Test
    @DisplayName("obtenirProfessionsDistinctes sans query: retourne une liste")
    void obtenirProfessionsDistinctes_noQuery_returnsList() {
        List<String> professions = membreService.obtenirProfessionsDistinctes(null);
        assertThat(professions).isNotNull();
    }

    @Test
    @DisplayName("obtenirProfessionsDistinctes avec query vide: retourne une liste")
    void obtenirProfessionsDistinctes_emptyQuery_returnsList() {
        List<String> professions = membreService.obtenirProfessionsDistinctes("");
        assertThat(professions).isNotNull();
    }

    @Test
    @DisplayName("obtenirProfessionsDistinctes avec query: retourne une liste filtrée")
    void obtenirProfessionsDistinctes_withQuery_returnsList() {
        List<String> professions = membreService.obtenirProfessionsDistinctes("Ingénieur");
        assertThat(professions).isNotNull();
    }

    // =========================================================================
    // exporterVersCSV / exporterVersPDF / genererModeleImport — error branches
    // covered by ExportImportDelegationTests above; these cover listerMembres
    // =========================================================================

    // =========================================================================
    // listerMembresPourExport
    // =========================================================================

    @Nested
    @DisplayName("listerMembresPourExport")
    class ListerMembresPourExportTests {

        @Test
        @DisplayName("sans associationId: liste tous les membres")
        void listerMembresPourExport_noAssociationId_listAll() {
            Membre m = membreFixture("export@test.dev");
            m.setMembresOrganisations(new ArrayList<>());
            when(membreRepository.listAll()).thenReturn(List.of(m));
            when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of());

            List<dev.lions.unionflow.server.api.dto.membre.response.MembreResponse> result =
                    membreService.listerMembresPourExport(null, null, null, null, null);

            assertThat(result).hasSize(1);
            verify(membreRepository).listAll();
        }

        @Test
        @DisplayName("filtre par statut ACTIF: garde seulement les membres actifs")
        void listerMembresPourExport_filtreActif() {
            Membre actif = membreFixture("actif@test.dev");
            actif.setActif(true);
            actif.setMembresOrganisations(new ArrayList<>());

            Membre inactif = membreFixture("inactif@test.dev");
            inactif.setActif(false);
            inactif.setMembresOrganisations(new ArrayList<>());

            when(membreRepository.listAll()).thenReturn(List.of(actif, inactif));
            when(membreRoleRepository.findActifsByMembreId(actif.getId())).thenReturn(List.of());

            List<dev.lions.unionflow.server.api.dto.membre.response.MembreResponse> result =
                    membreService.listerMembresPourExport(null, "ACTIF", null, null, null);

            assertThat(result).hasSize(1);
            assertThat(result.get(0).getEmail()).isEqualTo("actif@test.dev");
        }

        @Test
        @DisplayName("filtre par statut INACTIF: garde seulement les membres inactifs")
        void listerMembresPourExport_filtreInactif() {
            Membre actif = membreFixture("actif2@test.dev");
            actif.setActif(true);
            actif.setMembresOrganisations(new ArrayList<>());

            Membre inactif = membreFixture("inactif2@test.dev");
            inactif.setActif(false);
            inactif.setMembresOrganisations(new ArrayList<>());

            when(membreRepository.listAll()).thenReturn(List.of(actif, inactif));
            when(membreRoleRepository.findActifsByMembreId(inactif.getId())).thenReturn(List.of());

            List<dev.lions.unionflow.server.api.dto.membre.response.MembreResponse> result =
                    membreService.listerMembresPourExport(null, "INACTIF", null, null, null);

            assertThat(result).hasSize(1);
            assertThat(result.get(0).getEmail()).isEqualTo("inactif2@test.dev");
        }

        @Test
        @DisplayName("statut vide: tous les membres retournés sans filtre statut")
        void listerMembresPourExport_statutVide_noFilter() {
            Membre m1 = membreFixture("e1@test.dev");
            m1.setActif(true);
            m1.setMembresOrganisations(new ArrayList<>());
            Membre m2 = membreFixture("e2@test.dev");
            m2.setActif(false);
            m2.setMembresOrganisations(new ArrayList<>());

            when(membreRepository.listAll()).thenReturn(List.of(m1, m2));
            when(membreRoleRepository.findActifsByMembreId(m1.getId())).thenReturn(List.of());
            when(membreRoleRepository.findActifsByMembreId(m2.getId())).thenReturn(List.of());

            List<dev.lions.unionflow.server.api.dto.membre.response.MembreResponse> result =
                    membreService.listerMembresPourExport(null, "", null, null, null);

            assertThat(result).hasSize(2);
        }
    }

    // =========================================================================
    // importerMembres (delegation)
    // =========================================================================

    @Test
    @DisplayName("importerMembres: délègue à membreImportExportService")
    void importerMembres_delegates() {
        java.io.InputStream is = new java.io.ByteArrayInputStream(new byte[0]);
        UUID orgId = UUID.randomUUID();
        dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport mockResult =
                new dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport();
        mockResult.totalLignes = 0;
        mockResult.lignesTraitees = 0;
        mockResult.lignesErreur = 0;
        mockResult.erreurs = new ArrayList<>();

        when(membreImportExportService.importerMembres(is, "test.csv", orgId, "ACTIF", false, true))
                .thenReturn(mockResult);

        dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport result =
                membreService.importerMembres(is, "test.csv", orgId, "ACTIF", false, true);

        assertThat(result).isNotNull();
        verify(membreImportExportService).importerMembres(is, "test.csv", orgId, "ACTIF", false, true);
    }

    // =========================================================================
    // lierMembreOrganisationEtIncrementerQuota
    // =========================================================================

    @Nested
    @DisplayName("lierMembreOrganisationEtIncrementerQuota")
    class LierMembreOrganisationTests {

        @Test
        @DisplayName("membre null: lève IllegalArgumentException")
        void lierMembre_nullMembre_throws() {
            assertThatThrownBy(() ->
                membreService.lierMembreOrganisationEtIncrementerQuota(null, UUID.randomUUID(), "ACTIF"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("obligatoires");
        }

        @Test
        @DisplayName("organisationId null: lève IllegalArgumentException")
        void lierMembre_nullOrganisationId_throws() {
            Membre m = membreFixture("lier@test.dev");
            assertThatThrownBy(() ->
                membreService.lierMembreOrganisationEtIncrementerQuota(m, null, "ACTIF"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("obligatoires");
        }

        @Test
        @DisplayName("organisation introuvable: lève IllegalArgumentException")
        void lierMembre_organisationIntrouvable_throws() {
            Membre m = membreFixture("lier2@test.dev");
            UUID orgId = UUID.randomUUID(); // UUID inexistant en base

            // EntityManager real + DB vide pour cet UUID → retourne null
            assertThatThrownBy(() ->
                membreService.lierMembreOrganisationEtIncrementerQuota(m, orgId, "ACTIF"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("Organisation non trouvée");
        }

        @Test
        @DisplayName("typeMembreDefaut non-ACTIF: statut EN_ATTENTE_VALIDATION")
        void lierMembre_typeNonActif_statutEnAttente() {
            // Vérification que le chemin EN_ATTENTE_VALIDATION est bien sélectionné
            // via l'enum (le test de l'organisation introuvable intervient après)
            Membre m = membreFixture("lier3@test.dev");
            UUID orgId = UUID.randomUUID();

            assertThatThrownBy(() ->
                membreService.lierMembreOrganisationEtIncrementerQuota(m, orgId, "EN_ATTENTE_VALIDATION"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("Organisation non trouvée");
        }
    }

    // =========================================================================
    // searchMembresAdvanced — branches ADMIN_ORGANISATION
    // =========================================================================

    @Nested
    @DisplayName("searchMembresAdvanced — branches sécurité")
    class SearchMembresAdvancedSecurityTests {

        @Test
        @DisplayName("ADMIN_ORGANISATION sans organisations: retourne résultat vide")
        void searchMembresAdvanced_adminOrg_noOrgs_returnsEmpty() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of());

            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));

            assertThat(result).isNotNull();
            assertThat(result.getTotalElements()).isEqualTo(0L);
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION avec organisations: restreint par orgIds")
        void searchMembresAdvanced_adminOrg_withOrgs_restrictsToOrgs() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

            UUID orgId = UUID.randomUUID();
            dev.lions.unionflow.server.entity.Organisation org = new dev.lions.unionflow.server.entity.Organisation();
            org.setId(orgId);

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of(org));

            // Appelle le vrai EntityManager (DB vide → totalElements = 0)
            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("ADMIN_ORGANISATION avec organisationIds en intersection: filtre correctement")
        void searchMembresAdvanced_adminOrg_intersectionOrgIds() {
            UUID orgId1 = UUID.randomUUID();
            UUID orgId2 = UUID.randomUUID();

            dev.lions.unionflow.server.entity.Organisation org1 = new dev.lions.unionflow.server.entity.Organisation();
            org1.setId(orgId1);

            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder()
                            .organisationIds(new ArrayList<>(List.of(orgId2)))
                            .build();

            Principal principal = () -> "admin@org.dev";
            when(securityIdentity.getPrincipal()).thenReturn(principal);
            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
            when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev"))
                    .thenReturn(List.of(org1));

            // L'intersection est vide → totalElements = 0
            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Utilisateur normal: recherche sans restriction")
        void searchMembresAdvanced_normalUser_noRestriction() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

            // Appelle le vrai EntityManager (DB vide ou non → pas d'exception)
            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Critères avec statut ACTIF: filtre par actif=true")
        void searchMembresAdvanced_avecStatutActif() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder()
                            .statut("ACTIF")
                            .build();

            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Critères avec includeInactifs: inclut tous")
        void searchMembresAdvanced_avecIncludeInactifs() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder()
                            .includeInactifs(true)
                            .build();

            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Critères avec statut non-ACTIF: inclut inactifs")
        void searchMembresAdvanced_avecStatutInactif() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder()
                            .statut("INACTIF")
                            .build();

            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Critères avec query, nom, prenom, email, telephone: construit la requête JPQL correctement")
        void searchMembresAdvanced_avecTousCriteres() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder()
                            .query("test")
                            .nom("Doe")
                            .prenom("John")
                            .email("john@test.dev")
                            .telephone("0600")
                            .build();

            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Sort avec direction Descending: clause ORDER BY contient DESC")
        void searchMembresAdvanced_sortDescending_construitClauseDesc() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

            // Sort.by("nom").descending() crée une colonne Direction.Descending → couvre la branche DESC
            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom").descending());

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Sort null: searchMembresAdvanced ne lève pas d'exception")
        void searchMembresAdvanced_sortNull_pasDException() {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

            // null sort → buildOrderByClause retourne "m.nom ASC" (branche default)
            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), null);

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Sort avec colonnes vides: buildOrderByClause retourne 'm.nom ASC' (branche isEmpty)")
        void searchMembresAdvanced_sortEmptyColumns_coversEmptyBranch() throws Exception {
            dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                    dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

            when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

            // Sort non-null mais avec colonnes vides → branche sort.getColumns().isEmpty()
            Sort emptySort;
            try {
                java.lang.reflect.Constructor<Sort> ctor = Sort.class.getDeclaredConstructor();
                ctor.setAccessible(true);
                emptySort = ctor.newInstance();
            } catch (NoSuchMethodException e) {
                return;
            }

            dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                    membreService.searchMembresAdvanced(criteria, Page.of(0, 10), emptySort);

            assertThat(result).isNotNull();
        }
    }

    // =========================================================================
    // listerMembresParOrganisations — avec liste non vide (EntityManager réel)
    // =========================================================================

    @Test
    @DisplayName("listerMembresParOrganisations: UUID inexistant → liste vide (aucun membre lié)")
    void listerMembresParOrganisations_uuidInexistant_listeVide() {
        // Passe une liste non vide avec un UUID inexistant → la requête JPQL retourne []
        List<Membre> result = membreService.listerMembresParOrganisations(
                List.of(UUID.randomUUID()),
                Page.of(0, 10),
                Sort.by("nom"));
        assertThat(result).isNotNull();
        assertThat(result).isEmpty();
    }

    @Test
    @DisplayName("listerMembresParOrganisations: sans pagination (page null) → ne lève pas d'exception")
    void listerMembresParOrganisations_pagingNull_pasDException() {
        List<Membre> result = membreService.listerMembresParOrganisations(
                List.of(UUID.randomUUID()),
                null,
                null);
        assertThat(result).isNotNull();
    }

    // =========================================================================
    // listerMembresPourExport — branche associationId != null
    // =========================================================================

    @Test
    @DisplayName("listerMembresPourExport avec associationId inexistant: retourne liste vide")
    void listerMembresPourExport_avecAssociationId_listeVide() {
        // EntityManager réel disponible dans @QuarkusTest — UUID inexistant → liste vide
        List<dev.lions.unionflow.server.api.dto.membre.response.MembreResponse> result =
                membreService.listerMembresPourExport(UUID.randomUUID(), null, null, null, null);

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

    @Test
    @DisplayName("listerMembresPourExport avec associationId et filtre ACTIF: filtre les membres")
    void listerMembresPourExport_avecAssociationIdEtFiltreActif_resultatFiltre() {
        UUID orgId = UUID.randomUUID(); // inexistant → 0 membres
        List<dev.lions.unionflow.server.api.dto.membre.response.MembreResponse> result =
                membreService.listerMembresPourExport(orgId, "ACTIF", null, null, null);

        assertThat(result).isNotNull();
    }

    // =========================================================================
    // Tests @TestTransaction (doivent être dans la classe externe — CDI interdit
    // les interceptor bindings sur les méthodes des classes internes JUnit @Nested)
    // =========================================================================

    @Test
    @TestTransaction
    @DisplayName("mettreAJourMembre: champs mis à jour correctement (même email)")
    void mettreAJourMembre_sameEmail_updatesFields() {
        // Persist without pre-set ID — let Hibernate generate it (@GeneratedValue UUID)
        Membre existing = new Membre();
        existing.setEmail("same@unionflow.dev");
        existing.setNom("Doe");
        existing.setPrenom("John");
        existing.setDateNaissance(LocalDate.of(1990, 1, 1));
        existing.setNumeroMembre("UF2024-SAME");
        existing.setActif(true);
        existing.setStatutCompte("ACTIF");
        existing.setMembresOrganisations(new ArrayList<>());
        existing.setAdresses(new ArrayList<>());
        em.persist(existing);
        em.flush();
        UUID id = existing.getId();

        Membre modifie = new Membre();
        modifie.setEmail("same@unionflow.dev");
        modifie.setNom("Smith");
        modifie.setPrenom("Jane");
        modifie.setTelephone("0600000001");
        modifie.setDateNaissance(LocalDate.of(1992, 3, 10));
        modifie.setActif(false);

        Membre updated = membreService.mettreAJourMembre(id, modifie);

        assertThat(updated.getNom()).isEqualTo("Smith");
        assertThat(updated.getPrenom()).isEqualTo("Jane");
        assertThat(updated.getTelephone()).isEqualTo("0600000001");
        assertThat(updated.getActif()).isFalse();
    }

    @Test
    @TestTransaction
    @DisplayName("mettreAJourMembre: email modifié, nouveau email disponible")
    void mettreAJourMembre_newEmailAvailable_updatesEmail() {
        Membre existing = new Membre();
        existing.setEmail("old@unionflow.dev");
        existing.setNom("Doe");
        existing.setPrenom("John");
        existing.setDateNaissance(LocalDate.of(1990, 1, 1));
        existing.setNumeroMembre("UF2024-OLD");
        existing.setActif(true);
        existing.setStatutCompte("ACTIF");
        existing.setMembresOrganisations(new ArrayList<>());
        existing.setAdresses(new ArrayList<>());
        em.persist(existing);
        em.flush();
        UUID id = existing.getId();

        Membre modifie = new Membre();
        modifie.setEmail("new@unionflow.dev");
        modifie.setNom("Smith");
        modifie.setPrenom("Jane");
        modifie.setDateNaissance(LocalDate.of(1990, 1, 1));
        modifie.setActif(true);

        Membre updated = membreService.mettreAJourMembre(id, modifie);

        assertThat(updated.getEmail()).isEqualTo("new@unionflow.dev");
    }

    @Test
    @TestTransaction
    @DisplayName("mettreAJourMembre: email modifié mais déjà pris → IllegalArgumentException")
    void mettreAJourMembre_emailAlreadyTaken_throws() {
        Membre existing = new Membre();
        existing.setEmail("old2@unionflow.dev");
        existing.setNom("Doe");
        existing.setPrenom("John");
        existing.setDateNaissance(LocalDate.of(1990, 1, 1));
        existing.setNumeroMembre("UF2024-OLD2");
        existing.setActif(true);
        existing.setStatutCompte("ACTIF");
        existing.setMembresOrganisations(new ArrayList<>());
        existing.setAdresses(new ArrayList<>());
        em.persist(existing);
        em.flush();
        UUID id = existing.getId();

        Membre modifie = new Membre();
        modifie.setEmail("taken@unionflow.dev");
        modifie.setNom("Smith");
        modifie.setPrenom("Jane");
        modifie.setDateNaissance(LocalDate.of(1990, 1, 1));

        when(membreRepository.findByEmail("taken@unionflow.dev"))
                .thenReturn(Optional.of(membreFixture("taken@unionflow.dev")));

        assertThatThrownBy(() -> membreService.mettreAJourMembre(id, modifie))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("email existe déjà");
    }

    @Test
    @TestTransaction
    @DisplayName("trouverParId: retourne le membre si trouvé")
    void trouverParId_found() {
        Membre m = new Membre();
        m.setEmail("find@unionflow.dev");
        m.setNom("Doe");
        m.setPrenom("John");
        m.setDateNaissance(LocalDate.of(1990, 1, 1));
        m.setNumeroMembre("UF2024-FIND");
        m.setActif(true);
        m.setStatutCompte("ACTIF");
        m.setMembresOrganisations(new ArrayList<>());
        m.setAdresses(new ArrayList<>());
        em.persist(m);
        em.flush();
        UUID generatedId = m.getId();

        Optional<Membre> result = membreService.trouverParId(generatedId);

        assertThat(result).isPresent();
        assertThat(result.get().getEmail()).isEqualTo("find@unionflow.dev");
    }

    @Test
    @TestTransaction
    @DisplayName("desactiverMembre: flag actif mis à false")
    void desactiverMembre_setsActifFalse() {
        Membre existing = new Membre();
        existing.setEmail("active@unionflow.dev");
        existing.setNom("Doe");
        existing.setPrenom("John");
        existing.setDateNaissance(LocalDate.of(1990, 1, 1));
        existing.setNumeroMembre("UF2024-ACTIVE");
        existing.setActif(true);
        existing.setStatutCompte("ACTIF");
        existing.setMembresOrganisations(new ArrayList<>());
        existing.setAdresses(new ArrayList<>());
        em.persist(existing);
        em.flush();
        UUID id = existing.getId();

        membreService.desactiverMembre(id);

        // existing is the managed entity — service modifies the same instance
        assertThat(existing.getActif()).isFalse();
    }

    // =========================================================================
    // getOrganisationIdsForCurrentUserIfAdminOrg — branches manquantes
    // =========================================================================

    @Test
    @DisplayName("listerMembres avec SUPER_ADMIN + ADMIN_ORGANISATION: pas de restriction (branche SUPER_ADMIN couverte)")
    void listerMembres_superAdminAndAdminOrg_returnsAll() {
        Page page = Page.of(0, 10);
        Sort sort = Sort.by("nom").ascending();
        List<Membre> all = List.of(membreFixture("super@test.dev"));

        Principal principal = () -> "super@org.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION", "SUPER_ADMIN"));
        when(membreRepository.findAll(page, sort)).thenReturn(all);

        List<Membre> result = membreService.listerMembres(page, sort);

        assertThat(result).hasSize(1);
    }

    @Test
    @DisplayName("listerMembres avec ADMIN_ORGANISATION + email null depuis principal: retourne tous les membres")
    void listerMembres_adminOrg_nullEmail_returnsAll() {
        Page page = Page.of(0, 10);
        Sort sort = Sort.by("nom").ascending();
        List<Membre> all = List.of(membreFixture("n@test.dev"));

        // Principal whose getName() returns null
        Principal principal = () -> null;
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(membreRepository.findAll(page, sort)).thenReturn(all);

        List<Membre> result = membreService.listerMembres(page, sort);

        assertThat(result).hasSize(1);
    }

    // =========================================================================
    // searchMembresAdvanced + buildOrderByClause — branche Descending avec données réelles
    // Nécessite totalElements > 0 pour que buildOrderByClause soit appelé
    // =========================================================================

    @Test
    @TestTransaction
    @DisplayName("searchMembresAdvanced avec données réelles et sort Descending couvre buildOrderByClause DESC")
    void searchMembresAdvanced_avecDonnees_sortDescending_couvreDescBranch() {
        // Persister un membre pour que totalElements > 0
        Membre m = new Membre();
        m.setEmail("search-desc-" + UUID.randomUUID() + "@test.com");
        m.setNom("Zorro");
        m.setPrenom("Test");
        m.setDateNaissance(LocalDate.of(1990, 1, 1));
        m.setNumeroMembre("UF-DESC-" + UUID.randomUUID().toString().substring(0, 8));
        m.setActif(true);
        m.setStatutCompte("ACTIF");
        m.setMembresOrganisations(new ArrayList<>());
        m.setAdresses(new ArrayList<>());
        em.persist(m);
        em.flush();

        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

        // Sort Descending → totalElements > 0 → buildOrderByClause appelé avec Descending → branche DESC couverte
        dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom").descending());

        assertThat(result).isNotNull();
        assertThat(result.getTotalElements()).isGreaterThan(0L);
    }

    @Test
    @TestTransaction
    @DisplayName("searchMembresAdvanced avec données réelles et sort colonnes vides couvre buildOrderByClause isEmpty()")
    void searchMembresAdvanced_avecDonnees_sortColonnesVides_couvreIsEmptyBranch() throws Exception {
        // Persister un membre pour que totalElements > 0 → buildOrderByClause est appelé
        Membre m = new Membre();
        m.setEmail("search-empty-sort-" + UUID.randomUUID() + "@test.com");
        m.setNom("Alpha");
        m.setPrenom("Test");
        m.setDateNaissance(LocalDate.of(1990, 1, 1));
        m.setNumeroMembre("UF-EMPTY-" + UUID.randomUUID().toString().substring(0, 8));
        m.setActif(true);
        m.setStatutCompte("ACTIF");
        m.setMembresOrganisations(new ArrayList<>());
        m.setAdresses(new ArrayList<>());
        em.persist(m);
        em.flush();

        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

        // Sort non-null mais colonnes vides → buildOrderByClause appelé → branche isEmpty() couverte
        Sort emptySort;
        try {
            java.lang.reflect.Constructor<Sort> ctor = Sort.class.getDeclaredConstructor();
            ctor.setAccessible(true);
            emptySort = ctor.newInstance();
        } catch (NoSuchMethodException e) {
            // Si le constructeur n'est pas accessible, le test passe sans erreur
            return;
        }

        dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                membreService.searchMembresAdvanced(criteria, Page.of(0, 10), emptySort);

        assertThat(result).isNotNull();
        assertThat(result.getTotalElements()).isGreaterThan(0L);
    }

    // =========================================================================
    // creerMembre — branche getNumeroMembre().isEmpty() = true (non-null mais vide)
    // =========================================================================

    @Test
    @TestTransaction
    @DisplayName("creerMembre avec numeroMembre vide ('') → génère un nouveau numéro (branche isEmpty()=true)")
    void creerMembre_emptyNumeroMembre_generatesNew() {
        Membre m = new Membre();
        m.setEmail("creer-empty-num-" + UUID.randomUUID() + "@test.dev");
        m.setNom("Vide");
        m.setPrenom("Num");
        m.setDateNaissance(LocalDate.of(1990, 1, 1));
        m.setNumeroMembre(""); // non-null mais vide → isEmpty()=true → génère un nouveau numéro
        m.setActif(true);
        m.setStatutCompte("ACTIF");
        m.setMembresOrganisations(new ArrayList<>());
        m.setAdresses(new ArrayList<>());

        Membre created = membreService.creerMembre(m);

        assertThat(created).isNotNull();
        assertThat(created.getNumeroMembre()).isNotNull().isNotEmpty();
    }

    // =========================================================================
    // getOrganisationIdsForCurrentUserIfAdminOrg — branche email.isBlank()=true
    // =========================================================================

    @Test
    @DisplayName("listerMembres avec ADMIN_ORGANISATION + email blanc depuis principal → retourne tous (branche isBlank=true L224)")
    void listerMembres_adminOrg_blankEmail_returnsAll() {
        Page page = Page.of(0, 10);
        Sort sort = Sort.by("nom").ascending();
        List<Membre> all = List.of(membreFixture("blank-email@test.dev"));

        // Principal dont getName() retourne une chaîne vide → isBlank()=true
        Principal principal = () -> "   ";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(membreRepository.findAll(page, sort)).thenReturn(all);

        List<Membre> result = membreService.listerMembres(page, sort);

        assertThat(result).hasSize(1);
    }

    // =========================================================================
    // getOrganisationIdsForCurrentUserIfAdminOrg — branche orgs == null (L226)
    // =========================================================================

    @Test
    @DisplayName("listerMembres avec ADMIN_ORGANISATION + service retourne null → retourne liste vide (branche orgs==null L226)")
    void listerMembres_adminOrg_nullOrgsList_returnsEmpty() {
        Page page = Page.of(0, 10);
        Sort sort = Sort.by("nom").ascending();

        Principal principal = () -> "admin-null-orgs@org.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        // listerOrganisationsPourUtilisateur retourne null → orgs == null = true → Optional.of(Set.of())
        when(organisationService.listerOrganisationsPourUtilisateur("admin-null-orgs@org.dev"))
                .thenReturn(null);

        List<Membre> result = membreService.listerMembres(page, sort);

        assertThat(result).isEmpty();
    }

    // =========================================================================
    // addSearchCriteria — branche getStatut()==null && includeInactifs==true (L595)
    // Couvre la branche "else if(!includeInactifs) = false" → rien n'est ajouté
    // =========================================================================

    @Test
    @TestTransaction
    @DisplayName("searchMembresAdvanced avec includeInactifs=true et statut=null → pas de filtre actif (branche else if false)")
    void searchMembresAdvanced_includeInactifsTrue_noActifFilter() {
        // Persister un membre inactif
        Membre m = new Membre();
        m.setEmail("search-inactif-" + UUID.randomUUID() + "@test.com");
        m.setNom("Inactif");
        m.setPrenom("Test");
        m.setDateNaissance(LocalDate.of(1990, 1, 1));
        m.setNumeroMembre("UF-INACT-" + UUID.randomUUID().toString().substring(0, 8));
        m.setActif(false); // inactif
        m.setStatutCompte("INACTIF");
        m.setMembresOrganisations(new ArrayList<>());
        m.setAdresses(new ArrayList<>());
        em.persist(m);
        em.flush();

        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        // includeInactifs=true, statut=null → condition `else if(!includeInactifs)` = false → pas de filtre actif
        dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder()
                        .includeInactifs(true)
                        .build();

        dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                membreService.searchMembresAdvanced(criteria, Page.of(0, 100), Sort.by("nom").ascending());

        assertThat(result).isNotNull();
        // Les membres inactifs sont inclus
        assertThat(result.getTotalElements()).isGreaterThan(0L);
    }

    // =========================================================================
    // lambda$calculateSearchStatistics$11:710 — branche r != null && !r.isEmpty() false (r = "")
    // =========================================================================

    @Test
    @TestTransaction
    @DisplayName("calculateSearchStatistics avec région vide ('') couvre la branche r != null && !r.isEmpty() = false")
    void searchMembresAdvanced_regionVide_brancheFalse() {
        // Persister un membre avec une adresse dont la région est une chaîne vide
        Membre m = new Membre();
        m.setEmail("search-region-empty-" + UUID.randomUUID() + "@test.com");
        m.setNom("RegionVide");
        m.setPrenom("Test");
        m.setDateNaissance(LocalDate.of(1990, 1, 1));
        m.setNumeroMembre("UF-REGV-" + UUID.randomUUID().toString().substring(0, 8));
        m.setActif(true);
        m.setStatutCompte("ACTIF");
        m.setMembresOrganisations(new ArrayList<>());

        // Adresse avec région = "" (non-null mais vide → !r.isEmpty() = false → filtrée)
        dev.lions.unionflow.server.entity.Adresse adresse = new dev.lions.unionflow.server.entity.Adresse();
        adresse.setRegion(""); // chaîne vide → filtrée par lambda
        adresse.setTypeAdresse("DOMICILE"); // NOT NULL constraint
        adresse.setMembre(m);
        m.setAdresses(List.of(adresse));

        em.persist(m);
        em.flush();

        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria =
                dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build();

        dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result =
                membreService.searchMembresAdvanced(criteria, Page.of(0, 100), Sort.by("nom").ascending());

        assertThat(result).isNotNull();
        assertThat(result.getTotalElements()).isGreaterThan(0L);
    }

    // =========================================================================
    // orgHasActiveSubscription (non-nested — @TestTransaction requires outer class)
    // =========================================================================

    @Test
    @DisplayName("orgHasActiveSubscription: orgId null → false (guard clause)")
    void orgHasActiveSubscription_nullOrgId_returnsFalse() {
        assertThat(membreService.orgHasActiveSubscription(null)).isFalse();
    }

    @Test
    @DisplayName("orgHasActiveSubscription: UUID inconnu (absent de la base) → false")
    void orgHasActiveSubscription_unknownOrgId_returnsFalse() {
        assertThat(membreService.orgHasActiveSubscription(UUID.randomUUID())).isFalse();
    }

    @Test
    @TestTransaction
    @DisplayName("orgHasActiveSubscription: Organisation avec souscription ACTIVE → true")
    void orgHasActiveSubscription_activeSubscription_returnsTrue() {
        Organisation org = new Organisation();
        org.setNom("Org-" + UUID.randomUUID());
        org.setTypeOrganisation("ASSOCIATION");
        org.setStatut("ACTIVE");
        org.setEmail("org-" + UUID.randomUUID() + "@test.dev");
        em.persist(org);

        // Réutiliser la formule existante (Flyway seed) ou en créer une si absente
        FormuleAbonnement formule = em.createQuery(
                "SELECT f FROM FormuleAbonnement f WHERE f.code = :code AND f.plage = :plage",
                FormuleAbonnement.class)
                .setParameter("code", TypeFormule.BASIC)
                .setParameter("plage", PlageMembres.PETITE)
                .getResultStream()
                .findFirst()
                .orElseGet(() -> {
                    FormuleAbonnement f = new FormuleAbonnement();
                    f.setCode(TypeFormule.BASIC);
                    f.setPlage(PlageMembres.PETITE);
                    f.setLibelle("Basic Petite");
                    f.setPrixMensuel(new java.math.BigDecimal("5.00"));
                    f.setPrixAnnuel(new java.math.BigDecimal("50.00"));
                    em.persist(f);
                    return f;
                });

        SouscriptionOrganisation souscription = new SouscriptionOrganisation();
        souscription.setOrganisation(org);
        souscription.setFormule(formule);
        souscription.setDateDebut(java.time.LocalDate.now());
        souscription.setDateFin(java.time.LocalDate.now().plusMonths(1));
        souscription.setStatut(StatutSouscription.ACTIVE);
        em.persist(souscription);
        em.flush();

        assertThat(membreService.orgHasActiveSubscription(org.getId())).isTrue();
    }
}
