package dev.lions.unionflow.server.resource;

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.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import jakarta.ws.rs.NotFoundException;
import java.util.Optional;
import java.util.UUID;

import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.service.MembreKeycloakSyncService;
import dev.lions.unionflow.server.service.MembreService;
import dev.lions.unionflow.server.service.MembreSuiviService;
import dev.lions.unionflow.server.service.OrganisationService;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;

import java.security.Principal;
import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * Tests complémentaires pour {@link MembreResource} couvrant les branches
 * inaccessibles via HTTP standard (nécessitent un appel CDI direct) :
 *
 * <ul>
 *   <li>{@code listerMembres} L103-106 : ADMIN_ORGANISATION avec email null/vide → liste vide</li>
 *   <li>{@code importerMembres} L578-581 : ADMIN_ORGANISATION avec email null → 401</li>
 * </ul>
 *
 * <p>Ces branches sont atteintes en injectant un mock {@link SecurityIdentity} dont
 * {@code getPrincipal()} retourne null ou dont {@code getName()} retourne une chaîne vide.
 * L'appel CDI direct contourne {@code @TestSecurity} qui fixe toujours un email non-vide.
 */
@QuarkusTest
class MembreResourceMissingBranchesTest {

    @Inject
    MembreResource membreResource;

    @InjectMock
    MembreService membreService;

    @InjectMock
    MembreKeycloakSyncService keycloakSyncService;

    @InjectMock
    MembreSuiviService membreSuiviService;

    @InjectMock
    OrganisationService organisationService;

    @InjectMock
    SecurityIdentity securityIdentity;

    // =========================================================================
    // listerMembres L103-106 : ADMIN_ORGANISATION + principal null → liste vide
    // =========================================================================

    /**
     * Couvre les lignes 99-106 de MembreResource :
     * {@code if (email == null || email.isEmpty()) { membres = List.of(); totalElements = 0; }}
     *
     * <p>Le principal est null → {@code email = null} → branche true → liste vide retournée.
     */
    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("listerMembres — ADMIN_ORGANISATION avec principal null retourne liste vide (couvre L103-106)")
    void listerMembres_adminOrg_principalNull_retourneListeVide() {
        // Simuler un ADMIN_ORGANISATION sans accès ADMIN ni SUPER_ADMIN
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        // Principal null → email = null
        when(securityIdentity.getPrincipal()).thenReturn(null);
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        PagedResponse<MembreSummaryResponse> result = membreResource.listerMembres(0, 20, "nom", "asc");

        assertThat(result).isNotNull();
        assertThat(result.getData()).isEmpty();
        assertThat(result.getTotal()).isEqualTo(0L);
    }

    /**
     * Couvre les lignes 99-106 de MembreResource :
     * Variante avec principal non-null mais {@code getName()} retourne une chaîne vide.
     */
    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("listerMembres — ADMIN_ORGANISATION avec email vide retourne liste vide (couvre L103-106 branche isEmpty)")
    void listerMembres_adminOrg_emailVide_retourneListeVide() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        // Principal non-null mais email vide
        Principal principal = () -> "";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        PagedResponse<MembreSummaryResponse> result = membreResource.listerMembres(0, 20, "nom", "asc");

        assertThat(result).isNotNull();
        assertThat(result.getData()).isEmpty();
        assertThat(result.getTotal()).isEqualTo(0L);
    }

    // =========================================================================
    // listerMembres L83-85 : branche tri desc avec ADMIN_ORGANISATION
    // =========================================================================

    /**
     * Couvre le tri descendant dans listerMembres pour le chemin ADMIN_ORGANISATION
     * (branche "desc" de Sort.by().descending()).
     */
    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("listerMembres — ADMIN_ORGANISATION avec tri desc couvre branche Sort descending")
    void listerMembres_adminOrg_triDesc_couvreDescendingSort() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        Principal principal = () -> "orgadmin@test.com";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(organisationService.listerOrganisationsPourUtilisateur(anyString())).thenReturn(List.of());
        when(membreService.listerMembresParOrganisations(any(), any(), any())).thenReturn(List.of());
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        PagedResponse<MembreSummaryResponse> result = membreResource.listerMembres(0, 20, "prenom", "desc");

        assertThat(result).isNotNull();
    }

    // =========================================================================
    // importerMembres L578-581 : ADMIN_ORGANISATION + email null → 401
    //
    // NOTE : Cette branche (L578-581) est difficile à atteindre en pratique car
    // getPrincipal() est null AND organisationId est fourni.
    // Le test via CDI direct est la seule façon de la couvrir sans modifier le code source.
    //
    // Cependant, importerMembres() nécessite un FileUpload (paramètre RestForm) qui ne peut
    // pas être simulé facilement via CDI direct. Cette branche constitue un dead code pratique
    // car si getPrincipal() est null, le framework Quarkus OIDC aurait rejeté la requête avant
    // d'atteindre cette ligne. Elle est documentée ici pour la transparence.
    // =========================================================================

    /**
     * Couvre la branche {@code email == null || email.isEmpty()} dans {@code listerMembres}
     * via SecurityIdentity mocké avec getRoles retournant null (→ roles = Set.of()).
     *
     * <p>Quand {@code securityIdentity.getRoles()} retourne null, le code utilise Set.of()
     * → {@code onlyOrgAdmin = false} → chemin global (ADMIN/SUPER_ADMIN).
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("listerMembres — getRoles null traité comme Set.of() → chemin global (couvre L88-90)")
    void listerMembres_rolesNull_traitesCommeSetVide_cheminGlobal() {
        when(securityIdentity.getRoles()).thenReturn(null);
        when(membreService.listerMembres(any(), any())).thenReturn(List.of());
        when(membreService.compterMembres()).thenReturn(0L);
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        PagedResponse<MembreSummaryResponse> result = membreResource.listerMembres(0, 20, "nom", "asc");

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

    // =========================================================================
    // creerMembre L187-190 : getRoles null → onlyOrgAdmin = false
    // =========================================================================

    /**
     * Couvre la branche {@code securityIdentity.getRoles() != null} dans {@code creerMembre}
     * quand getRoles retourne null → Set.of() → onlyOrgAdmin = false → chemin normal ADMIN.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("creerMembre — getRoles null traité comme Set.of() → chemin non-orgAdmin (couvre L187-190)")
    void creerMembre_rolesNull_traitesCommeSetVide() {
        when(securityIdentity.getRoles()).thenReturn(null);

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

        dev.lions.unionflow.server.api.dto.membre.response.MembreResponse responseDto =
                new dev.lions.unionflow.server.api.dto.membre.response.MembreResponse();
        responseDto.setId(membre.getId());

        when(membreService.convertFromCreateRequest(any())).thenReturn(membre);
        when(membreService.creerMembre(any(Membre.class))).thenReturn(membre);
        when(membreService.convertToResponse(any(Membre.class))).thenReturn(responseDto);

        dev.lions.unionflow.server.api.dto.membre.request.CreateMembreRequest request =
                new dev.lions.unionflow.server.api.dto.membre.request.CreateMembreRequest(
                        "Jean",
                        "Dupont",
                        "jean.dupont.nullroles@test.com",
                        null,
                        null,
                        java.time.LocalDate.of(1990, 5, 15),
                        null, null, null, null, null, null,
                        null
                );

        Response response = membreResource.creerMembre(request);

        assertThat(response.getStatus()).isEqualTo(201);
    }

    // =========================================================================
    // importerMembres L567-571 : ADMIN_ORGANISATION + principal null → 401
    // =========================================================================

    /**
     * Couvre la branche {@code email == null || email.isEmpty()} dans {@code importerMembres}
     * (lignes 567-571) : ADMIN_ORGANISATION avec organisationId fourni mais principal null
     * → Response 401.
     */
    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("importerMembres — ADMIN_ORGANISATION avec principal null retourne 401 (couvre L567-571)")
    void importerMembres_adminOrg_principalNull_retourne401() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(securityIdentity.getPrincipal()).thenReturn(null);

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("test.xlsx");

        java.util.UUID orgId = java.util.UUID.randomUUID();

        Response response = membreResource.importerMembres(
                fileUpload, "test.xlsx", orgId.toString(), "ACTIF", false, false);

        assertThat(response.getStatus()).isEqualTo(Response.Status.UNAUTHORIZED.getStatusCode());
    }

    // =========================================================================
    // importerMembres L591-592 : IOException lors de Files.newInputStream → RuntimeException
    // =========================================================================

    /**
     * Couvre le bloc {@code catch (java.io.IOException e)} de {@code importerMembres}
     * (lignes 591-592) : lorsque {@code Files.newInputStream(file.uploadedFile())} lève une
     * {@code FileNotFoundException} (chemin inexistant), le catch la convertit en
     * {@code RuntimeException}.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("importerMembres — Files.newInputStream échoue (chemin inexistant) → RuntimeException (couvre L591-592)")
    void importerMembres_ioException_couvreL591L592() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("test.xlsx");
        // Path inexistant → Files.newInputStream lance FileNotFoundException (sous-classe IOException)
        when(fileUpload.uploadedFile()).thenReturn(java.nio.file.Paths.get("/nonexistent/path/file.xlsx"));

        assertThatThrownBy(() -> membreResource.importerMembres(
                fileUpload, "test.xlsx", null, "ACTIF", false, false))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Erreur lors de la lecture du fichier");
    }

    // =========================================================================
    // importerMembres L540 : file == null → 400 (branche file null via CDI direct)
    // =========================================================================

    /**
     * Couvre la branche {@code file == null} dans {@code importerMembres} (L540) :
     * lorsque le FileUpload est null → Response 400.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("importerMembres — file null → 400 BAD_REQUEST (couvre L540 file==null branche)")
    void importerMembres_fileNull_retourne400() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        Response response = membreResource.importerMembres(
                null, "test.xlsx", null, "ACTIF", false, false);

        assertThat(response.getStatus()).isEqualTo(Response.Status.BAD_REQUEST.getStatusCode());
    }

    // =========================================================================
    // importerMembres L548 : organisationIdStr="" → isEmpty()=true → organisationId=null
    // =========================================================================

    /**
     * Couvre la branche A=true && B=false de L548 :
     * organisationIdStr="" → !isEmpty()=false → condition false → organisationId=null.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("importerMembres — organisationIdStr='' → isEmpty=true → organisationId=null (couvre L548 A=true,B=false)")
    void importerMembres_organisationIdStrEmpty_organisationIdNull() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("test.xlsx");
        when(fileUpload.uploadedFile()).thenReturn(java.nio.file.Paths.get("/nonexistent/path/test.xlsx"));

        // organisationIdStr="" → !isEmpty() = false → organisationId = null
        assertThatThrownBy(() -> membreResource.importerMembres(
                fileUpload, "test.xlsx", "", "ACTIF", false, false))
                .isInstanceOf(RuntimeException.class);
    }

    // =========================================================================
    // importerMembres L554 : roles == null → onlyOrgAdmin = false (A=false branch)
    // =========================================================================

    /**
     * Couvre la branche A=false de L554 :
     * roles == null → (null != null) = false → onlyOrgAdmin=false.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("importerMembres — roles=null → onlyOrgAdmin=false (couvre L554 A=false)")
    void importerMembres_rolesNull_onlyOrgAdminFalse() {
        when(securityIdentity.getRoles()).thenReturn(null); // roles = null → A=false

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("test.xlsx");
        when(fileUpload.uploadedFile()).thenReturn(java.nio.file.Paths.get("/nonexistent/path/test.xlsx"));

        // roles=null → onlyOrgAdmin=false → chemin global → IOException
        assertThatThrownBy(() -> membreResource.importerMembres(
                fileUpload, "test.xlsx", null, "ACTIF", false, false))
                .isInstanceOf(RuntimeException.class);
    }

    // =========================================================================
    // importerMembres L568 : email.isEmpty() = true → 401 (B=true branch)
    // =========================================================================

    /**
     * Couvre la branche B=true de L568 :
     * email != null mais email = "" → isEmpty()=true → 401.
     */
    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("importerMembres — ADMIN_ORGANISATION avec email vide → 401 (couvre L568 email.isEmpty B=true)")
    void importerMembres_adminOrg_emailVide_retourne401() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        Principal principalVide = () -> ""; // getName() retourne "" → email.isEmpty()=true
        when(securityIdentity.getPrincipal()).thenReturn(principalVide);

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("test.xlsx");

        UUID orgId = UUID.randomUUID();
        Response response = membreResource.importerMembres(
                fileUpload, "test.xlsx", orgId.toString(), "ACTIF", false, false);

        assertThat(response.getStatus()).isEqualTo(Response.Status.UNAUTHORIZED.getStatusCode());
    }

    // =========================================================================
    // importerMembres L554 : roles.contains("ADMIN_ORGANISATION") && roles.contains("ADMIN")
    // → onlyOrgAdmin = false (ADMIN_ORGANISATION + ADMIN → chemin global)
    // =========================================================================

    /**
     * Couvre la branche où roles contient SUPER_ADMIN → onlyOrgAdmin=false (L554: !contains SUPER_ADMIN = false).
     */
    @Test
    @TestSecurity(user = "superadmin@test.com", roles = {"ADMIN_ORGANISATION", "SUPER_ADMIN"})
    @DisplayName("importerMembres — ADMIN_ORGANISATION+SUPER_ADMIN → !contains SUPER_ADMIN=false → onlyOrgAdmin=false (couvre L554)")
    void importerMembres_adminOrgEtSuperAdmin_onlyOrgAdminFalse() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION", "SUPER_ADMIN"));

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("test.xlsx");
        when(fileUpload.uploadedFile()).thenReturn(java.nio.file.Paths.get("/nonexistent/path/file.xlsx"));

        // ADMIN_ORGANISATION + SUPER_ADMIN → !contains SUPER_ADMIN = false → onlyOrgAdmin=false
        assertThatThrownBy(() -> membreResource.importerMembres(
                fileUpload, "test.xlsx", null, "ACTIF", false, false))
                .isInstanceOf(RuntimeException.class);
    }

    /**
     * Couvre la branche AND dans {@code importerMembres} (L554) :
     * ADMIN_ORGANISATION + ADMIN → {@code !roles.contains("ADMIN")} = false → onlyOrgAdmin=false.
     * Le fichier a un chemin valide inexistant → RuntimeException (après la vérif L554).
     */
    @Test
    @TestSecurity(user = "adminorg@test.com", roles = {"ADMIN_ORGANISATION", "ADMIN"})
    @DisplayName("importerMembres — ADMIN_ORGANISATION+ADMIN → onlyOrgAdmin=false (couvre L554 !contains ADMIN branche)")
    void importerMembres_adminOrgEtAdmin_onlyOrgAdminFalse() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION", "ADMIN"));

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("test.xlsx");
        when(fileUpload.uploadedFile()).thenReturn(java.nio.file.Paths.get("/nonexistent/path/file.xlsx"));

        // onlyOrgAdmin=false → passe directement à la lecture du fichier → IOException
        assertThatThrownBy(() -> membreResource.importerMembres(
                fileUpload, "test.xlsx", null, "ACTIF", false, false))
                .isInstanceOf(RuntimeException.class);
    }

    // =========================================================================
    // importerMembres L540 : fileName == null → uses file.fileName() (branche true)
    // =========================================================================

    /**
     * Couvre la branche true de {@code if (fileName == null || fileName.isEmpty())} (L540) :
     * fileName=null → fileName est remplacé par file.fileName().
     * Le fichier pointe vers un chemin inexistant → RuntimeException.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("importerMembres — fileName=null → L540 true → fileName=file.fileName() (couvre branche null L540)")
    void importerMembres_fileNameNull_usesFileFileName() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("from-file.xlsx");
        when(fileUpload.uploadedFile()).thenReturn(java.nio.file.Paths.get("/nonexistent/path/from-file.xlsx"));

        // fileName=null → L540: null == null = true → fileName = file.fileName()
        assertThatThrownBy(() -> membreResource.importerMembres(
                fileUpload, null, null, "ACTIF", false, false))
                .isInstanceOf(RuntimeException.class);
    }

    /**
     * Couvre la branche true via chaîne vide de {@code if (fileName == null || fileName.isEmpty())} (L540) :
     * fileName="" → isEmpty()=true → L540 true → fileName = file.fileName().
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("importerMembres — fileName='' → L540 isEmpty true → fileName=file.fileName() (couvre branche isEmpty L540)")
    void importerMembres_fileNameEmpty_usesFileFileName() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("from-file-empty.xlsx");
        when(fileUpload.uploadedFile()).thenReturn(java.nio.file.Paths.get("/nonexistent/path/from-file-empty.xlsx"));

        // fileName="" → L540: isEmpty()=true → fileName = file.fileName()
        assertThatThrownBy(() -> membreResource.importerMembres(
                fileUpload, "", null, "ACTIF", false, false))
                .isInstanceOf(RuntimeException.class);
    }

    // =========================================================================
    // importerMembres L548 : typeMembreDefaut == null → uses "ACTIF" (branche true)
    // =========================================================================

    /**
     * Couvre la branche true de {@code if (typeMembreDefaut == null || typeMembreDefaut.isEmpty())} (L548) :
     * typeMembreDefaut=null → remplacé par "ACTIF".
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("importerMembres — typeMembreDefaut=null → L548 true → uses 'ACTIF' (couvre branche null L548)")
    void importerMembres_typeMembreDefautNull_usesActif() {
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        org.jboss.resteasy.reactive.multipart.FileUpload fileUpload =
                mock(org.jboss.resteasy.reactive.multipart.FileUpload.class);
        when(fileUpload.size()).thenReturn(100L);
        when(fileUpload.fileName()).thenReturn("test.xlsx");
        when(fileUpload.uploadedFile()).thenReturn(java.nio.file.Paths.get("/nonexistent/path/test.xlsx"));

        // typeMembreDefaut=null → L548: null == null = true → typeMembreDefaut = "ACTIF"
        assertThatThrownBy(() -> membreResource.importerMembres(
                fileUpload, "test.xlsx", null, null, false, false))
                .isInstanceOf(RuntimeException.class);
    }

    // =========================================================================
    // obtenirMembre L135 : filter(m -> m.getActif() == null || m.getActif())
    // Branche manquante : membre trouvé mais actif == false → 404 via NotFoundException
    // =========================================================================

    /**
     * Couvre la branche false du filtre dans {@code obtenirMembre} (L135) :
     * {@code m.getActif() == null || m.getActif()} — quand actif=false le filtre
     * renvoie empty → orElseThrow → NotFoundException.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("obtenirMembre — membre trouvé mais actif=false → NotFoundException (couvre L135 false branch)")
    void obtenirMembre_membreInactif_leveNotFoundException() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setActif(false); // actif=false → filtre rejette → orElseThrow

        when(membreService.trouverParId(membreId)).thenReturn(Optional.of(membre));

        assertThatThrownBy(() -> membreResource.obtenirMembre(membreId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    // =========================================================================
    // obtenirMembreConnecte L163 : filter(m -> m.getActif() == null || m.getActif())
    // Branche manquante : membre trouvé mais actif == false → 404 via NotFoundException
    // =========================================================================

    /**
     * Couvre la branche d'activation automatique dans {@code obtenirMembreConnecte} :
     * quand actif=false, le code appelle {@code activerMembre} et retourne 200.
     */
    @Test
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    @DisplayName("obtenirMembreConnecte — membre trouvé mais actif=false → activation auto → 200")
    void obtenirMembreConnecte_membreInactif_activeEtRetourne200() {
        Principal principal = () -> "membre@test.com";
        when(securityIdentity.getPrincipal()).thenReturn(principal);

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

        Membre membreActif = new Membre();
        membreActif.setId(membre.getId());
        membreActif.setActif(true);

        dev.lions.unionflow.server.api.dto.membre.response.MembreResponse responseDto =
                new dev.lions.unionflow.server.api.dto.membre.response.MembreResponse();
        responseDto.setId(membreActif.getId());
        responseDto.setRoles(List.of("MEMBRE"));

        when(membreService.trouverParEmail("membre@test.com")).thenReturn(Optional.of(membre));
        when(membreService.activerMembre(membre.getId())).thenReturn(membreActif);
        when(membreService.convertToResponse(membreActif)).thenReturn(responseDto);

        Response response = membreResource.obtenirMembreConnecte();

        assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode());
    }

    // =========================================================================
    // affecterOrganisation — L752-764
    // Cette méthode n'était pas couverte (7 lignes manquantes).
    // =========================================================================

    /**
     * Couvre la branche {@code organisationId == null} (L757-760) :
     * quand organisationId n'est pas fourni → 400 BAD_REQUEST.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("affecterOrganisation — organisationId null → 400 BAD_REQUEST (couvre L757-760)")
    void affecterOrganisation_organisationIdNull_retourne400() {
        UUID membreId = UUID.randomUUID();

        Response response = membreResource.affecterOrganisation(membreId, null);

        assertThat(response.getStatus()).isEqualTo(Response.Status.BAD_REQUEST.getStatusCode());
    }

    /**
     * Couvre le happy path de {@code affecterOrganisation} (L763-764) :
     * membre et organisation valides → 200 OK avec le membre mis à jour.
     */
    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("affecterOrganisation — happy path → 200 OK (couvre L763-764)")
    void affecterOrganisation_happyPath_retourne200() {
        UUID membreId = UUID.randomUUID();
        UUID organisationId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setNom("Test");
        membre.setPrenom("Affect");
        membre.setEmail("affect@test.dev");

        dev.lions.unionflow.server.api.dto.membre.response.MembreResponse responseDto =
                new dev.lions.unionflow.server.api.dto.membre.response.MembreResponse();
        responseDto.setId(membreId);

        when(membreService.affecterOrganisation(membreId, organisationId)).thenReturn(membre);
        when(membreService.convertToResponse(membre)).thenReturn(responseDto);

        Response response = membreResource.affecterOrganisation(membreId, organisationId);

        assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode());
    }
}
