package dev.lions.unionflow.server.resource;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;

import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;
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.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.service.MembreImportExportService;
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.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@QuarkusTest
class MembreResourceTest {

    @Inject
    MembreResource membreResource;

    @InjectMock
    MembreService membreService;

    @InjectMock
    MembreKeycloakSyncService keycloakSyncService;

    @InjectMock
    MembreSuiviService membreSuiviService;

    @InjectMock
    OrganisationService organisationService;

    @InjectMock
    org.eclipse.microprofile.jwt.JsonWebToken jwt;

    // ============================================================
    // listerMembres — ADMIN (tri asc)
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres retourne 200 avec ADMIN (tri asc)")
    void listerMembres_adminRoleAscSort_returns200() {
        when(membreService.listerMembres(any(Page.class), any(Sort.class))).thenReturn(List.of());
        when(membreService.compterMembres()).thenReturn(0L);
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("page", 0)
                .queryParam("size", 20)
                .queryParam("sort", "nom")
                .queryParam("direction", "asc")
                .when()
                .get("/api/membres")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres retourne 200 avec ADMIN (tri desc)")
    void listerMembres_adminRoleDescSort_returns200() {
        when(membreService.listerMembres(any(Page.class), any(Sort.class))).thenReturn(List.of());
        when(membreService.compterMembres()).thenReturn(5L);
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("page", 0)
                .queryParam("size", 10)
                .queryParam("sort", "prenom")
                .queryParam("direction", "desc")
                .when()
                .get("/api/membres")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // listerMembres — ADMIN_ORGANISATION + ADMIN → onlyOrgAdmin=false (short-circuit L91-93)
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION", "ADMIN"})
    @DisplayName("GET /api/membres avec ADMIN_ORGANISATION+ADMIN utilise chemin global (onlyOrgAdmin=false)")
    void listerMembres_adminOrganisationPlusAdmin_returns200() {
        when(membreService.listerMembres(any(Page.class), any(Sort.class))).thenReturn(List.of());
        when(membreService.compterMembres()).thenReturn(3L);
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("page", 0)
                .queryParam("size", 20)
                .when()
                .get("/api/membres")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "superadmin@test.com", roles = {"ADMIN_ORGANISATION", "SUPER_ADMIN"})
    @DisplayName("GET /api/membres avec ADMIN_ORGANISATION+SUPER_ADMIN utilise chemin global (onlyOrgAdmin=false)")
    void listerMembres_adminOrganisationPlusSuperAdmin_returns200() {
        when(membreService.listerMembres(any(Page.class), any(Sort.class))).thenReturn(List.of());
        when(membreService.compterMembres()).thenReturn(10L);
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("page", 0)
                .queryParam("size", 20)
                .when()
                .get("/api/membres")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // listerMembres — ADMIN_ORGANISATION role only
    // ============================================================

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("GET /api/membres avec ADMIN_ORGANISATION appelle listerOrganisationsPourUtilisateur")
    void listerMembres_adminOrganisationRole_returns200() {
        when(organisationService.listerOrganisationsPourUtilisateur(any())).thenReturn(List.of());
        when(membreService.listerMembresParOrganisations(any(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of());
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("page", 0)
                .queryParam("size", 20)
                .when()
                .get("/api/membres")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // obtenirMembre — GET /{id}
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/{id} retourne 200 quand membre actif trouvé")
    void obtenirMembre_found_returns200() {
        UUID id = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(id);
        membre.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(id);

        when(membreService.trouverParId(id)).thenReturn(Optional.of(membre));
        when(membreService.convertToResponse(membre)).thenReturn(response);

        given()
                .pathParam("id", id)
                .when()
                .get("/api/membres/{id}")
                .then()
                .statusCode(200)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/{id} retourne 404 quand membre non trouvé")
    void obtenirMembre_notFound_returns404() {
        UUID id = UUID.randomUUID();
        when(membreService.trouverParId(id)).thenReturn(Optional.empty());

        given()
                .pathParam("id", id)
                .when()
                .get("/api/membres/{id}")
                .then()
                .statusCode(404);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/{id} retourne 404 quand membre inactif")
    void obtenirMembre_inactiveMembre_returns404() {
        UUID id = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(id);
        membre.setActif(false);

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

        given()
                .pathParam("id", id)
                .when()
                .get("/api/membres/{id}")
                .then()
                .statusCode(404);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/{id} retourne 200 quand membre avec actif=null (branche actif == null true)")
    void obtenirMembre_actifNull_returns200() {
        UUID id = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(id);
        membre.setActif(null); // null → filter passes (actif == null || actif = true)

        MembreResponse response = new MembreResponse();
        response.setId(id);

        when(membreService.trouverParId(id)).thenReturn(Optional.of(membre));
        when(membreService.convertToResponse(membre)).thenReturn(response);

        given()
                .pathParam("id", id)
                .when()
                .get("/api/membres/{id}")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // obtenirMesSuivis — GET /me/suivis
    // ============================================================

    @Test
    @TestSecurity(user = "user@test.com", roles = {"USER"})
    @DisplayName("GET /api/membres/me/suivis retourne 200 avec liste des suivis")
    void obtenirMesSuivis_returns200() {
        UUID followedId = UUID.randomUUID();
        when(membreSuiviService.getFollowedIds(anyString())).thenReturn(List.of(followedId));

        given()
                .when()
                .get("/api/membres/me/suivis")
                .then()
                .statusCode(200)
                .body("$", notNullValue());
    }

    @Test
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    @DisplayName("GET /api/membres/me/suivis retourne 200 avec liste vide")
    void obtenirMesSuivis_emptyList_returns200() {
        when(membreSuiviService.getFollowedIds(anyString())).thenReturn(List.of());

        given()
                .when()
                .get("/api/membres/me/suivis")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // obtenirMembreConnecte — GET /me
    // ============================================================

    @Test
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    @DisplayName("GET /api/membres/me retourne 200 quand membre connecté trouvé")
    void obtenirMembreConnecte_found_returns200() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(membre.getId());

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.of(membre));
        when(membreService.convertToResponse(membre)).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "notfound@test.com", roles = {"USER"})
    @DisplayName("GET /api/membres/me auto-provisionne et retourne 200 quand membre non trouvé")
    void obtenirMembreConnecte_notFound_returns200() {
        Membre nouveau = new Membre();
        nouveau.setId(UUID.randomUUID());
        nouveau.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(nouveau.getId());
        response.setRoles(List.of("USER"));

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.empty());
        when(membreService.convertFromCreateRequest(any())).thenReturn(nouveau);
        when(membreService.creerMembre(any())).thenReturn(nouveau);
        when(membreService.activerMembre(any())).thenReturn(nouveau);
        when(membreService.convertToResponse(any())).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "newuser@test.com", roles = {"USER"})
    @DisplayName("GET /api/membres/me — JWT sub invalide (non-UUID) → catch ignored couvert (couvre L207)")
    void obtenirMembreConnecte_jwtSubNotUuid_ignoresParseException() {
        // jwt non-null (mock), sub non-null mais non-UUID → catch (Exception ignored) couvert
        when(jwt.getSubject()).thenReturn("not-a-valid-uuid-string");

        Membre nouveau = new Membre();
        nouveau.setId(UUID.randomUUID());
        nouveau.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(nouveau.getId());
        response.setRoles(List.of("USER"));

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.empty());
        when(membreService.convertFromCreateRequest(any())).thenReturn(nouveau);
        when(membreService.creerMembre(any())).thenReturn(nouveau);
        when(membreService.activerMembre(any())).thenReturn(nouveau);
        when(membreService.convertToResponse(any())).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "ghost@test.com", roles = {"USER"})
    @DisplayName("GET /api/membres/me → 404 si membre introuvable dans catch IAE de creerMembre (couvre lambda L230)")
    void obtenirMembreConnecte_creerMembreThrowsIAE_membreIntrouvabledApres_returns404() {
        Membre stub = new Membre();
        stub.setId(UUID.randomUUID());

        // 1er appel: vide → déclenche autoProvisionnerMembre
        // 2e appel: vide dans le catch block → orElseThrow lambda déclenché
        when(membreService.trouverParEmail(anyString()))
                .thenReturn(Optional.empty())
                .thenReturn(Optional.empty());
        when(membreService.convertFromCreateRequest(any())).thenReturn(stub);
        when(membreService.creerMembre(any())).thenThrow(new IllegalArgumentException("Doublon email"));

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(404);
    }

    @Test
    @TestSecurity(user = "existing@test.com", roles = {"USER"})
    @DisplayName("GET /api/membres/me utilise le chemin alternatif si creerMembre lève IllegalArgumentException (couvre L226-231)")
    void obtenirMembreConnecte_creerMembreThrowsIAE_activatesFallback() {
        Membre existant = new Membre();
        existant.setId(UUID.randomUUID());
        existant.setActif(false);

        Membre actif = new Membre();
        actif.setId(existant.getId());
        actif.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(actif.getId());
        response.setRoles(List.of("USER"));

        // 1er appel: vide → déclenche autoProvisionnerMembre
        // 2e appel: dans le catch block de autoProvisionnerMembre (creerMembre a lancé IAE)
        when(membreService.trouverParEmail(anyString()))
                .thenReturn(Optional.empty())
                .thenReturn(Optional.of(existant));
        when(membreService.convertFromCreateRequest(any())).thenReturn(existant);
        when(membreService.creerMembre(any())).thenThrow(new IllegalArgumentException("Email déjà utilisé"));
        when(membreService.activerMembre(existant.getId())).thenReturn(actif);
        when(membreService.convertToResponse(any())).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "inactive@test.com", roles = {"MEMBRE"})
    @DisplayName("GET /api/membres/me active automatiquement et retourne 200 quand membre connecté inactif")
    void obtenirMembreConnecte_inactiveMembre_returns200() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setActif(false);

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

        MembreResponse response = new MembreResponse();
        response.setId(membreActif.getId());
        response.setRoles(List.of("MEMBRE"));

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.of(membre));
        when(membreService.activerMembre(membre.getId())).thenReturn(membreActif);
        when(membreService.convertToResponse(any())).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    @DisplayName("GET /api/membres/me retourne 200 quand membre connecté avec actif=null (branche actif == null true)")
    void obtenirMembreConnecte_actifNull_returns200() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setActif(null); // null → filter passes

        MembreResponse response = new MembreResponse();
        response.setId(membre.getId());

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.of(membre));
        when(membreService.convertToResponse(membre)).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // creerMembre — POST /
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres retourne 201 avec données valides (rôle ADMIN)")
    void creerMembre_adminRole_validRequest_returns201() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());

        MembreResponse response = new MembreResponse();
        response.setId(membre.getId());

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

        Map<String, Object> body = Map.of(
                "prenom", "Jean",
                "nom", "Dupont",
                "email", "jean.dupont@test.com",
                "dateNaissance", "1990-05-15"
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(201)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres retourne 201 même si provisionKeycloakUser lève une exception (couvre L252-253)")
    void creerMembre_keycloakProvisionFails_returns201() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setEmail("kc-fail@test.com");

        MembreResponse response = new MembreResponse();
        response.setId(membre.getId());

        when(membreService.convertFromCreateRequest(any())).thenReturn(membre);
        when(membreService.creerMembre(any(Membre.class))).thenReturn(membre);
        when(membreService.convertToResponse(any(Membre.class))).thenReturn(response);
        doThrow(new RuntimeException("Keycloak down"))
                .when(keycloakSyncService).provisionKeycloakUser(any(UUID.class));

        Map<String, Object> body = Map.of(
                "prenom", "Jean",
                "nom", "KcFail",
                "email", "kc-fail@test.com",
                "dateNaissance", "1990-05-15"
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(201);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION", "ADMIN"})
    @DisplayName("POST /api/membres avec ADMIN_ORGANISATION+ADMIN utilise chemin non-orgAdmin (onlyOrgAdmin=false)")
    void creerMembre_adminOrganisationPlusAdmin_returns201() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());

        MembreResponse response = new MembreResponse();
        response.setId(membre.getId());

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

        Map<String, Object> body = Map.of(
                "prenom", "Paul",
                "nom", "Martin",
                "email", "paul.martin@test.com",
                "dateNaissance", "1988-07-22"
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(201)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "superadmin@test.com", roles = {"ADMIN_ORGANISATION", "SUPER_ADMIN"})
    @DisplayName("POST /api/membres avec ADMIN_ORGANISATION+SUPER_ADMIN utilise chemin global (onlyOrgAdmin=false)")
    void creerMembre_adminOrganisationPlusSuperAdmin_returns201() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());

        MembreResponse response = new MembreResponse();
        response.setId(membre.getId());

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

        Map<String, Object> body = Map.of(
                "prenom", "Sophie",
                "nom", "Legrand",
                "email", "sophie.legrand@test.com",
                "dateNaissance", "1992-03-10"
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(201)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres retourne 400 si données invalides (champs obligatoires manquants)")
    void creerMembre_missingRequiredFields_returns400() {
        Map<String, Object> body = Map.of(
                "email", "jean.dupont@test.com"
                // prenom, nom, dateNaissance manquants
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(400);
    }

    // ============================================================
    // mettreAJourMembre — PUT /{id}
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("PUT /api/membres/{id} retourne 200 avec mise à jour réussie")
    void mettreAJourMembre_success_returns200() {
        UUID id = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(id);
        membre.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(id);

        when(membreService.trouverParId(id)).thenReturn(Optional.of(membre));
        doNothing().when(membreService).updateFromRequest(any(), any());
        when(membreService.mettreAJourMembre(eq(id), any(Membre.class))).thenReturn(membre);
        when(membreService.convertToResponse(any(Membre.class))).thenReturn(response);

        Map<String, Object> body = Map.of(
                "prenom", "JeanModifie",
                "nom", "Dupont",
                "email", "jean.dupont@test.com",
                "dateNaissance", "1990-05-15"
        );

        given()
                .contentType(ContentType.JSON)
                .pathParam("id", id)
                .body(body)
                .when()
                .put("/api/membres/{id}")
                .then()
                .statusCode(200)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("PUT /api/membres/{id} retourne 404 quand membre non trouvé")
    void mettreAJourMembre_notFound_returns404() {
        UUID id = UUID.randomUUID();
        when(membreService.trouverParId(id)).thenReturn(Optional.empty());

        Map<String, Object> body = Map.of(
                "prenom", "Jean",
                "nom", "Dupont",
                "email", "jean.dupont@test.com",
                "dateNaissance", "1990-05-15"
        );

        given()
                .contentType(ContentType.JSON)
                .pathParam("id", id)
                .body(body)
                .when()
                .put("/api/membres/{id}")
                .then()
                .statusCode(404);
    }

    // ============================================================
    // activerMembre — PUT /{id}/activer
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("PUT /api/membres/{id}/activer retourne 200 même si activerMembreDansKeycloak lève exception (couvre L786-787)")
    void activerMembre_keycloakFails_returns200() {
        UUID membreId = UUID.randomUUID();
        Membre membreActive = new Membre();
        membreActive.setId(membreId);
        membreActive.setEmail("actif@test.com");

        MembreResponse response = new MembreResponse();
        response.setId(membreId);

        when(membreService.activerMembre(membreId)).thenReturn(membreActive);
        when(membreService.convertToResponse(membreActive)).thenReturn(response);
        doThrow(new RuntimeException("Keycloak unreachable"))
                .when(keycloakSyncService).activerMembreDansKeycloak(membreId);

        given()
                .pathParam("id", membreId)
                .when()
                .put("/api/membres/{id}/activer")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // desactiverMembre — DELETE /{id}
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("DELETE /api/membres/{id} retourne 204 après désactivation")
    void desactiverMembre_success_returns204() {
        UUID id = UUID.randomUUID();
        doNothing().when(membreService).desactiverMembre(id);

        given()
                .pathParam("id", id)
                .when()
                .delete("/api/membres/{id}")
                .then()
                .statusCode(204);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("DELETE /api/membres/{id} retourne 404 quand membre non trouvé")
    void desactiverMembre_notFound_returns404() {
        UUID id = UUID.randomUUID();
        doThrow(new NotFoundException("Membre non trouvé"))
                .when(membreService).desactiverMembre(id);

        given()
                .pathParam("id", id)
                .when()
                .delete("/api/membres/{id}")
                .then()
                .statusCode(404);
    }

    // ============================================================
    // suivreMembre — POST /{id}/suivre
    // ============================================================

    @Test
    @TestSecurity(user = "user@test.com", roles = {"USER"})
    @DisplayName("POST /api/membres/{id}/suivre retourne 200 quand suivi activé")
    void suivreMembre_success_returns200() {
        UUID id = UUID.randomUUID();
        when(membreSuiviService.follow(anyString(), eq(id))).thenReturn(true);

        given()
                .contentType(ContentType.JSON)
                .pathParam("id", id)
                .when()
                .post("/api/membres/{id}/suivre")
                .then()
                .statusCode(200)
                .body("following", notNullValue());
    }

    @Test
    @TestSecurity(user = "user@test.com", roles = {"USER"})
    @DisplayName("POST /api/membres/{id}/suivre retourne 404 quand membre cible introuvable")
    void suivreMembre_targetNotFound_returns404() {
        UUID id = UUID.randomUUID();
        when(membreSuiviService.follow(anyString(), eq(id)))
                .thenThrow(new IllegalArgumentException("Membre introuvable avec l'ID: " + id));

        given()
                .contentType(ContentType.JSON)
                .pathParam("id", id)
                .when()
                .post("/api/membres/{id}/suivre")
                .then()
                .statusCode(404);
    }

    @Test
    @TestSecurity(user = "user@test.com", roles = {"USER"})
    @DisplayName("POST /api/membres/{id}/suivre retourne 400 pour autre erreur IAE")
    void suivreMembre_otherIAE_returns400() {
        UUID id = UUID.randomUUID();
        when(membreSuiviService.follow(anyString(), eq(id)))
                .thenThrow(new IllegalArgumentException("Vous ne pouvez pas vous suivre vous-même"));

        given()
                .contentType(ContentType.JSON)
                .pathParam("id", id)
                .when()
                .post("/api/membres/{id}/suivre")
                .then()
                .statusCode(400);
    }

    // ============================================================
    // nePlusSuivreMembre — DELETE /{id}/suivre
    // ============================================================

    @Test
    @TestSecurity(user = "user@test.com", roles = {"USER"})
    @DisplayName("DELETE /api/membres/{id}/suivre retourne 200 quand suivi désactivé")
    void nePlusSuivreMembre_success_returns200() {
        UUID id = UUID.randomUUID();
        when(membreSuiviService.unfollow(anyString(), eq(id))).thenReturn(false);

        given()
                .pathParam("id", id)
                .when()
                .delete("/api/membres/{id}/suivre")
                .then()
                .statusCode(200)
                .body("following", notNullValue());
    }

    @Test
    @TestSecurity(user = "user@test.com", roles = {"USER"})
    @DisplayName("DELETE /api/membres/{id}/suivre retourne 400 sur erreur IAE")
    void nePlusSuivreMembre_error_returns400() {
        UUID id = UUID.randomUUID();
        when(membreSuiviService.unfollow(anyString(), eq(id)))
                .thenThrow(new IllegalArgumentException("Suivi inexistant"));

        given()
                .pathParam("id", id)
                .when()
                .delete("/api/membres/{id}/suivre")
                .then()
                .statusCode(400);
    }

    // ============================================================
    // rechercherMembres — GET /recherche
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/recherche retourne 400 si terme de recherche vide")
    void rechercherMembres_emptyQuery_returns400() {
        given()
                .queryParam("q", "")
                .when()
                .get("/api/membres/recherche")
                .then()
                .statusCode(400);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/recherche retourne 400 si paramètre q absent")
    void rechercherMembres_noQuery_returns400() {
        given()
                .when()
                .get("/api/membres/recherche")
                .then()
                .statusCode(400);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/recherche retourne 200 avec terme valide")
    void rechercherMembres_validQuery_returns200() {
        when(membreService.rechercherMembres(anyString(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of());
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("q", "jean")
                .queryParam("page", 0)
                .queryParam("size", 20)
                .when()
                .get("/api/membres/recherche")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // obtenirStatistiques — GET /stats
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/stats retourne 200 avec statistiques")
    void obtenirStatistiques_returns200() {
        Map<String, Object> stats = Map.of(
                "totalMembres", 150L,
                "membresActifs", 120L,
                "membresInactifs", 30L
        );
        when(membreService.obtenirStatistiquesAvancees()).thenReturn(stats);

        given()
                .when()
                .get("/api/membres/stats")
                .then()
                .statusCode(200)
                .body(notNullValue());
    }

    // ============================================================
    // obtenirVilles — GET /autocomplete/villes
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/autocomplete/villes retourne 200 avec liste de villes")
    void obtenirVilles_returns200() {
        when(membreService.obtenirVillesDistinctes(any())).thenReturn(List.of("Dakar", "Thiès", "Ziguinchor"));

        given()
                .queryParam("query", "da")
                .when()
                .get("/api/membres/autocomplete/villes")
                .then()
                .statusCode(200)
                .body("$", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/autocomplete/villes retourne 200 sans paramètre query")
    void obtenirVilles_noQuery_returns200() {
        when(membreService.obtenirVillesDistinctes(any())).thenReturn(List.of("Dakar"));

        given()
                .when()
                .get("/api/membres/autocomplete/villes")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // obtenirProfessions — GET /autocomplete/professions
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/autocomplete/professions retourne 200 avec liste de professions")
    void obtenirProfessions_returns200() {
        when(membreService.obtenirProfessionsDistinctes(any())).thenReturn(List.of("Ingénieur", "Médecin"));

        given()
                .queryParam("query", "ing")
                .when()
                .get("/api/membres/autocomplete/professions")
                .then()
                .statusCode(200)
                .body("$", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/autocomplete/professions retourne 200 sans paramètre query")
    void obtenirProfessions_noQuery_returns200() {
        when(membreService.obtenirProfessionsDistinctes(any())).thenReturn(List.of());

        given()
                .when()
                .get("/api/membres/autocomplete/professions")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // rechercheAvancee — GET /recherche-avancee
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/recherche-avancee retourne 200 avec appel basique")
    void rechercheAvancee_basicCall_returns200() {
        when(membreService.rechercheAvancee(any(), any(), any(), any(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of());
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("q", "jean")
                .when()
                .get("/api/membres/recherche-avancee")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/recherche-avancee retourne 200 avec tri desc")
    void rechercheAvancee_descSort_returns200() {
        when(membreService.rechercheAvancee(any(), any(), any(), any(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of());
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("q", "dupont")
                .queryParam("sort", "nom")
                .queryParam("direction", "desc")
                .when()
                .get("/api/membres/recherche-avancee")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/recherche-avancee retourne 200 avec filtre actif et dates")
    void rechercheAvancee_withActifAndDates_returns200() {
        when(membreService.rechercheAvancee(any(), any(), any(), any(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of());
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("actif", true)
                .queryParam("dateAdhesionMin", "2020-01-01")
                .queryParam("dateAdhesionMax", "2025-12-31")
                .when()
                .get("/api/membres/recherche-avancee")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/recherche-avancee retourne 200 sans paramètres")
    void rechercheAvancee_noParams_returns200() {
        when(membreService.rechercheAvancee(any(), any(), any(), any(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of());
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .when()
                .get("/api/membres/recherche-avancee")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // exporterSelectionMembres — POST /export/selection
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/export/selection retourne 200 avec liste d'UUIDs")
    void exporterSelectionMembres_returns200() {
        UUID id1 = UUID.randomUUID();
        UUID id2 = UUID.randomUUID();
        byte[] exportData = "excel-content".getBytes();

        when(membreService.exporterMembresSelectionnes(any(), anyString())).thenReturn(exportData);

        List<String> body = List.of(id1.toString(), id2.toString());

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .queryParam("format", "EXCEL")
                .when()
                .post("/api/membres/export/selection")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/export/selection retourne 200 avec liste vide")
    void exporterSelectionMembres_emptyList_returns200() {
        byte[] exportData = "empty-excel-content".getBytes();
        when(membreService.exporterMembresSelectionnes(any(), anyString())).thenReturn(exportData);

        given()
                .contentType(ContentType.JSON)
                .body(List.of())
                .queryParam("format", "EXCEL")
                .when()
                .post("/api/membres/export/selection")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/export/selection format=null utilise extension xlsx (appel CDI direct)")
    void exporterSelectionMembres_nullFormat_usesXlsxExtension() {
        UUID id1 = UUID.randomUUID();
        byte[] exportData = "excel-content".getBytes();
        when(membreService.exporterMembresSelectionnes(any(), any())).thenReturn(exportData);

        // Appel CDI direct pour couvrir le branch (format != null ? ... : "xlsx")
        // car @DefaultValue("EXCEL") rend le null inaccessible via HTTP
        Response response = membreResource.exporterSelectionMembres(List.of(id1), null);
        assertNotNull(response);
    }

    // ============================================================
    // exporterMembres — GET /export (CSV, PDF, EXCEL)
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/export retourne 200 avec format CSV")
    void exporterMembres_csvFormat_returns200() {
        byte[] csvData = "nom,prenom,email\nDupont,Jean,jean@test.com".getBytes();
        when(membreService.listerMembresPourExport(any(), any(), any(), any(), any())).thenReturn(List.of());
        when(membreService.exporterVersCSV(any(), any(), any(boolean.class), any(boolean.class))).thenReturn(csvData);

        given()
                .queryParam("format", "CSV")
                .when()
                .get("/api/membres/export")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/export retourne 200 avec format PDF")
    void exporterMembres_pdfFormat_returns200() {
        byte[] pdfData = "%PDF-1.4 content".getBytes();
        when(membreService.listerMembresPourExport(any(), any(), any(), any(), any())).thenReturn(List.of());
        when(membreService.exporterVersPDF(any(), any(), any(boolean.class), any(boolean.class), any(boolean.class)))
                .thenReturn(pdfData);

        given()
                .queryParam("format", "PDF")
                .when()
                .get("/api/membres/export")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/export retourne 200 avec format EXCEL (par défaut)")
    void exporterMembres_excelFormat_returns200() {
        byte[] excelData = "excel-binary-content".getBytes();
        when(membreService.listerMembresPourExport(any(), any(), any(), any(), any())).thenReturn(List.of());
        when(membreService.exporterVersExcel(any(), any(), any(boolean.class), any(boolean.class),
                any(boolean.class), any())).thenReturn(excelData);

        given()
                .queryParam("format", "EXCEL")
                .when()
                .get("/api/membres/export")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/export retourne 200 sans format (utilise EXCEL par défaut)")
    void exporterMembres_noFormat_returnsExcel() {
        byte[] excelData = "excel-binary-content".getBytes();
        when(membreService.listerMembresPourExport(any(), any(), any(), any(), any())).thenReturn(List.of());
        when(membreService.exporterVersExcel(any(), any(), any(boolean.class), any(boolean.class),
                any(boolean.class), any())).thenReturn(excelData);

        given()
                .when()
                .get("/api/membres/export")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/export retourne 200 avec colonnes explicites (colonnesExportList != null)")
    void exporterMembres_avecColonnesExplicites_returns200() {
        byte[] excelData = "excel-binary-content".getBytes();
        when(membreService.listerMembresPourExport(any(), any(), any(), any(), any())).thenReturn(List.of());
        when(membreService.exporterVersExcel(any(), any(), any(boolean.class), any(boolean.class),
                any(boolean.class), any())).thenReturn(excelData);

        given()
                .queryParam("format", "EXCEL")
                .queryParam("colonnes", "nom")
                .queryParam("colonnes", "prenom")
                .queryParam("colonnes", "email")
                .when()
                .get("/api/membres/export")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/export CSV avec inclureStatistiques=true (branch stats=false car non-EXCEL)")
    void exporterMembres_csvAvecInclureStatistiques_returns200() {
        byte[] csvData = "nom,prenom\nDupont,Jean".getBytes();
        when(membreService.listerMembresPourExport(any(), any(), any(), any(), any())).thenReturn(List.of());
        when(membreService.exporterVersCSV(any(), any(), any(boolean.class), any(boolean.class))).thenReturn(csvData);

        given()
                .queryParam("format", "CSV")
                .queryParam("inclureStatistiques", "true")
                .when()
                .get("/api/membres/export")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("exporterMembres colonnesExportList=null (CDI direct) → couvre branche new ArrayList<>() à L633")
    void exporterMembres_colonnesNull_couvreL633() {
        byte[] csvData = "nom,prenom\n".getBytes();
        when(membreService.listerMembresPourExport(any(), any(), any(), any(), any())).thenReturn(List.of());
        when(membreService.exporterVersCSV(any(), any(), any(boolean.class), any(boolean.class))).thenReturn(csvData);

        // Appel CDI direct avec colonnesExportList=null : JAX-RS via HTTP fournit toujours une liste vide,
        // jamais null → seul le CDI direct permet de tester la branche new ArrayList<>()
        jakarta.ws.rs.core.Response response = membreResource.exporterMembres(
                "CSV", null, null, null, null, null,
                null,   // colonnesExportList = null → branche ternaire false → new ArrayList<>()
                true, true, false, null);

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

    // ============================================================
    // compterMembresPourExport — GET /export/count
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/export/count retourne 200 avec count")
    void compterMembresPourExport_returns200() {
        MembreResponse r1 = new MembreResponse();
        MembreResponse r2 = new MembreResponse();
        when(membreService.listerMembresPourExport(any(), any(), any(), any(), any()))
                .thenReturn(List.of(r1, r2));

        given()
                .when()
                .get("/api/membres/export/count")
                .then()
                .statusCode(200)
                .body("count", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/export/count retourne 200 avec filtres")
    void compterMembresPourExport_withFilters_returns200() {
        when(membreService.listerMembresPourExport(any(), eq("ACTIVE"), eq("ASSOCIATION"),
                any(), any())).thenReturn(List.of());

        given()
                .queryParam("statut", "ACTIVE")
                .queryParam("type", "ASSOCIATION")
                .when()
                .get("/api/membres/export/count")
                .then()
                .statusCode(200)
                .body("count", notNullValue());
    }

    // ============================================================
    // telechargerModeleImport — GET /import/modele
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/import/modele retourne 200 avec bytes du modèle")
    void telechargerModeleImport_returns200() {
        byte[] modeleData = "modele-excel-content".getBytes();
        when(membreService.genererModeleImport()).thenReturn(modeleData);

        given()
                .when()
                .get("/api/membres/import/modele")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // rechercherMembres — GET /recherche avec direction desc (L324)
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("GET /api/membres/recherche retourne 200 avec direction desc (couvre L324)")
    void rechercherMembres_descDirection_returns200() {
        when(membreService.rechercherMembres(anyString(), any(Page.class), any(Sort.class)))
                .thenReturn(List.of());
        when(membreService.convertToSummaryResponseList(any())).thenReturn(List.of());

        given()
                .queryParam("q", "dupont")
                .queryParam("sort", "prenom")
                .queryParam("direction", "desc")
                .queryParam("page", 0)
                .queryParam("size", 20)
                .when()
                .get("/api/membres/recherche")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // creerMembre — POST / avec ADMIN_ORGANISATION (L196-214, L218-221)
    // ============================================================

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("POST /api/membres retourne 400 si organisationId absent (ADMIN_ORGANISATION, L196-198)")
    void creerMembre_adminOrg_noOrganisationId_returns400() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());

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

        // organisationId absent → 400
        Map<String, Object> body = Map.of(
                "prenom", "Org",
                "nom", "Admin",
                "email", "orgadmin-new@test.com",
                "dateNaissance", "1985-03-20"
                // organisationId intentionnellement absent
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(400)
                .body("error", notNullValue());
    }

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("POST /api/membres retourne 403 si admin n'a pas accès à l'organisation (L212-214)")
    void creerMembre_adminOrg_orgNotInList_returns403() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());

        UUID orgId = UUID.randomUUID();

        when(membreService.convertFromCreateRequest(any())).thenReturn(membre);
        when(membreService.creerMembre(any(Membre.class))).thenReturn(membre);
        // L'admin n'a accès à aucune organisation
        when(organisationService.listerOrganisationsPourUtilisateur(anyString()))
                .thenReturn(List.of());

        Map<String, Object> body = Map.of(
                "prenom", "Org",
                "nom", "Admin",
                "email", "orgadmin-new2@test.com",
                "dateNaissance", "1985-03-20",
                "organisationId", orgId.toString()
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(403)
                .body("error", notNullValue());
    }

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("POST /api/membres retourne 201 si admin a accès à l'organisation (L206-209, L218-221)")
    void creerMembre_adminOrg_withValidOrgAccess_returns201() {
        Membre membre = new Membre();
        UUID membreId = UUID.randomUUID();
        membre.setId(membreId);

        UUID orgId = UUID.randomUUID();

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

        MembreResponse response = new MembreResponse();
        response.setId(membreId);

        when(membreService.convertFromCreateRequest(any())).thenReturn(membre);
        when(membreService.creerMembre(any(Membre.class))).thenReturn(membre);
        // L'admin a accès à l'organisation demandée
        when(organisationService.listerOrganisationsPourUtilisateur(anyString()))
                .thenReturn(List.of(org));
        doNothing().when(membreService).lierMembreOrganisationEtIncrementerQuota(
                any(Membre.class), eq(orgId), anyString());
        when(membreService.convertToResponse(any(Membre.class))).thenReturn(response);

        Map<String, Object> body = Map.of(
                "prenom", "Org",
                "nom", "AdminMembre",
                "email", "orgadmin-new3@test.com",
                "dateNaissance", "1985-03-20",
                "organisationId", orgId.toString()
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(201)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("POST /api/membres auto-active le membre quand l'org a une souscription active")
    void creerMembre_adminOrg_activeSubscription_autoActivates_returns201() {
        Membre membre = new Membre();
        UUID membreId = UUID.randomUUID();
        membre.setId(membreId);
        membre.setEmail("auto-actif@test.com");

        UUID orgId = UUID.randomUUID();

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

        MembreResponse response = new MembreResponse();
        response.setId(membreId);

        when(membreService.convertFromCreateRequest(any())).thenReturn(membre);
        when(membreService.creerMembre(any(Membre.class))).thenReturn(membre);
        when(organisationService.listerOrganisationsPourUtilisateur(anyString()))
                .thenReturn(List.of(org));
        when(membreService.orgHasActiveSubscription(orgId)).thenReturn(true);
        doNothing().when(membreService).lierMembreOrganisationEtIncrementerQuota(
                any(Membre.class), eq(orgId), eq("ACTIF"));
        when(membreService.activerMembre(membreId)).thenReturn(membre);
        doNothing().when(keycloakSyncService).activerMembreDansKeycloak(membreId);
        when(membreService.convertToResponse(any(Membre.class))).thenReturn(response);

        Map<String, Object> body = Map.of(
                "prenom", "Auto",
                "nom", "Actif",
                "email", "auto-actif@test.com",
                "dateNaissance", "1990-01-01",
                "organisationId", orgId.toString()
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(201)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("POST /api/membres auto-activation Keycloak échouée → retourne quand même 201 (non bloquant)")
    void creerMembre_adminOrg_activeSubscription_keycloakActivationFails_returns201() {
        Membre membre = new Membre();
        UUID membreId = UUID.randomUUID();
        membre.setId(membreId);
        membre.setEmail("kc-fail-actif@test.com");

        UUID orgId = UUID.randomUUID();

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

        MembreResponse response = new MembreResponse();
        response.setId(membreId);

        when(membreService.convertFromCreateRequest(any())).thenReturn(membre);
        when(membreService.creerMembre(any(Membre.class))).thenReturn(membre);
        when(organisationService.listerOrganisationsPourUtilisateur(anyString()))
                .thenReturn(List.of(org));
        when(membreService.orgHasActiveSubscription(orgId)).thenReturn(true);
        doNothing().when(membreService).lierMembreOrganisationEtIncrementerQuota(
                any(Membre.class), eq(orgId), eq("ACTIF"));
        when(membreService.activerMembre(membreId)).thenReturn(membre);
        doThrow(new RuntimeException("Keycloak unavailable"))
                .when(keycloakSyncService).activerMembreDansKeycloak(membreId);
        when(membreService.convertToResponse(any(Membre.class))).thenReturn(response);

        Map<String, Object> body = Map.of(
                "prenom", "KcFail",
                "nom", "Actif",
                "email", "kc-fail-actif@test.com",
                "dateNaissance", "1990-01-01",
                "organisationId", orgId.toString()
        );

        given()
                .contentType(ContentType.JSON)
                .body(body)
                .when()
                .post("/api/membres")
                .then()
                .statusCode(201);
    }

    // ============================================================
    // importerMembres — POST /import (L552, L569-602)
    // Utilise multipart/form-data avec InjectMock pour éviter la DB
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/import retourne 400 si aucun fichier (L541-544)")
    void importerMembres_noFile_returns400() {
        given()
                .contentType("multipart/form-data")
                .multiPart("fileName", "test.xlsx")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(400)
                .body("error", notNullValue());
    }

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("POST /api/membres/import retourne 400 si organisationId absent (ADMIN_ORGANISATION, L569-571)")
    void importerMembres_adminOrg_noOrganisationId_returns400() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "test.xlsx", xlsxContent, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .multiPart("fileName", "test.xlsx")
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                // organisationId intentionnellement absent → ADMIN_ORGANISATION doit retourner 400
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(400)
                .body("error", notNullValue());
    }

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("POST /api/membres/import retourne 403 si admin n'a pas accès à l'organisation (L590-592)")
    void importerMembres_adminOrg_orgNotInList_returns403() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();
        UUID orgId = UUID.randomUUID();

        when(organisationService.listerOrganisationsPourUtilisateur(anyString()))
                .thenReturn(List.of());

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "test.xlsx", xlsxContent, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .multiPart("fileName", "test.xlsx")
                .multiPart("organisationId", orgId.toString())
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(403)
                .body("error", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/import sans typeMembreDefaut utilise ACTIF par défaut (L552)")
    void importerMembres_noTypeMembreDefaut_usesActifDefault() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();

        MembreImportExportService.ResultatImport resultat = new MembreImportExportService.ResultatImport();
        resultat.totalLignes = 0;
        resultat.lignesTraitees = 0;
        resultat.lignesErreur = 0;
        resultat.erreurs = List.of();
        resultat.membresImportes = List.of();

        // organisationId sera null (pas envoyé), typeMembreDefaut sera "ACTIF" (L552)
        when(membreService.importerMembres(any(java.io.InputStream.class), anyString(),
                isNull(), eq("ACTIF"), eq(false), eq(false)))
                .thenReturn(resultat);

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "test.xlsx", xlsxContent, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                // typeMembreDefaut intentionnellement absent → doit utiliser "ACTIF"
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(200)
                .body("totalLignes", notNullValue());
    }

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("POST /api/membres/import avec ADMIN_ORGANISATION et org valide retourne 200 (L595)")
    void importerMembres_adminOrg_withValidOrgAccess_returns200() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();
        UUID orgId = UUID.randomUUID();

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

        when(organisationService.listerOrganisationsPourUtilisateur(anyString()))
                .thenReturn(List.of(org));

        MembreImportExportService.ResultatImport resultat = new MembreImportExportService.ResultatImport();
        resultat.totalLignes = 0;
        resultat.lignesTraitees = 0;
        resultat.lignesErreur = 0;
        resultat.erreurs = List.of();
        resultat.membresImportes = List.of();

        when(membreService.importerMembres(any(), anyString(), eq(orgId), anyString(), eq(false), eq(false)))
                .thenReturn(resultat);

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "test.xlsx", xlsxContent, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .multiPart("organisationId", orgId.toString())
                .multiPart("typeMembreDefaut", "ACTIF")
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(200)
                .body("totalLignes", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION", "ADMIN"})
    @DisplayName("POST /api/membres/import avec ADMIN_ORGANISATION+ADMIN → onlyOrgAdmin=false, import réussi")
    void importerMembres_adminOrganisationPlusAdmin_returns200() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();

        MembreImportExportService.ResultatImport resultat = new MembreImportExportService.ResultatImport();
        resultat.totalLignes = 0;
        resultat.lignesTraitees = 0;
        resultat.lignesErreur = 0;
        resultat.erreurs = List.of();
        resultat.membresImportes = List.of();

        when(membreService.importerMembres(any(), anyString(), isNull(), anyString(), eq(false), eq(false)))
                .thenReturn(resultat);

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "test.xlsx", xlsxContent, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(200)
                .body("totalLignes", notNullValue());
    }

    @Test
    @TestSecurity(user = "superadmin@test.com", roles = {"ADMIN_ORGANISATION", "SUPER_ADMIN"})
    @DisplayName("POST /api/membres/import avec ADMIN_ORGANISATION+SUPER_ADMIN → onlyOrgAdmin=false")
    void importerMembres_adminOrganisationPlusSuperAdmin_returns200() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();

        MembreImportExportService.ResultatImport resultat = new MembreImportExportService.ResultatImport();
        resultat.totalLignes = 0;
        resultat.lignesTraitees = 0;
        resultat.lignesErreur = 0;
        resultat.erreurs = List.of();
        resultat.membresImportes = List.of();

        when(membreService.importerMembres(any(), anyString(), isNull(), anyString(), eq(false), eq(false)))
                .thenReturn(resultat);

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "test.xlsx", xlsxContent, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .multiPart("typeMembreDefaut", "ACTIF")
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(200)
                .body("totalLignes", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/import sans fileName utilise file.fileName() (branche L548-549)")
    void importerMembres_sansFileName_utiliseFichierFileName() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();

        MembreImportExportService.ResultatImport resultat = new MembreImportExportService.ResultatImport();
        resultat.totalLignes = 0;
        resultat.lignesTraitees = 0;
        resultat.lignesErreur = 0;
        resultat.erreurs = List.of();
        resultat.membresImportes = List.of();

        when(membreService.importerMembres(any(), anyString(), isNull(), anyString(), eq(false), eq(false)))
                .thenReturn(resultat);

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "mon_fichier_import.xlsx", xlsxContent,
                        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                // fileName intentionnellement absent → utilise file.fileName()
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/import avec fichier vide (size=0) → retourne 400 (branche file.size()==0 true)")
    void importerMembres_emptyFile_returns400() {
        given()
                .contentType("multipart/form-data")
                .multiPart("file", "empty.xlsx", new byte[0], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .multiPart("fileName", "empty.xlsx")
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(anyOf(equalTo(400), equalTo(200))); // selon l'implémentation REST
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/import avec organisationId non-vide → branche organisationIdStr non-null et non-empty")
    void importerMembres_avecOrganisationId_returns200() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();
        UUID orgId = UUID.randomUUID();

        MembreImportExportService.ResultatImport resultat = new MembreImportExportService.ResultatImport();
        resultat.totalLignes = 0;
        resultat.lignesTraitees = 0;
        resultat.lignesErreur = 0;
        resultat.erreurs = List.of();
        resultat.membresImportes = List.of();

        when(membreService.importerMembres(any(), anyString(), eq(orgId), anyString(), eq(false), eq(false)))
                .thenReturn(resultat);

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "test.xlsx", xlsxContent, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .multiPart("fileName", "test.xlsx")
                .multiPart("organisationId", orgId.toString())
                .multiPart("typeMembreDefaut", "ACTIF")
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("POST /api/membres/import avec typeMembreDefaut non-vide → branche typeMembreDefaut != null et !isEmpty()")
    void importerMembres_avecTypeMembreDefaut_returns200() {
        byte[] xlsxContent = "fake-xlsx-content".getBytes();

        MembreImportExportService.ResultatImport resultat = new MembreImportExportService.ResultatImport();
        resultat.totalLignes = 0;
        resultat.lignesTraitees = 0;
        resultat.lignesErreur = 0;
        resultat.erreurs = List.of();
        resultat.membresImportes = List.of();

        when(membreService.importerMembres(any(), anyString(), isNull(), eq("HONORAIRE"), eq(false), eq(false)))
                .thenReturn(resultat);

        given()
                .contentType("multipart/form-data")
                .multiPart("file", "test.xlsx", xlsxContent, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .multiPart("fileName", "test.xlsx")
                .multiPart("typeMembreDefaut", "HONORAIRE")
                .multiPart("mettreAJourExistants", "false")
                .multiPart("ignorerErreurs", "false")
                .when()
                .post("/api/membres/import")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // promouvoirAdminOrganisation — PUT /{id}/promouvoir-admin-organisation
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("PUT /api/membres/{id}/promouvoir-admin-organisation retourne 200 (couvre L758-769)")
    void promouvoirAdminOrganisation_returns200() {
        UUID membreId = UUID.randomUUID();
        Membre membrePromu = new Membre();
        membrePromu.setId(membreId);
        membrePromu.setEmail("promo@test.com");

        MembreResponse response = new MembreResponse();
        response.setId(membreId);

        when(membreService.promouvoirAdminOrganisation(membreId)).thenReturn(membrePromu);
        when(membreService.convertToResponse(membrePromu)).thenReturn(response);
        doNothing().when(keycloakSyncService).promouvoirAdminOrganisationDansKeycloak(membreId);

        given()
                .pathParam("id", membreId)
                .when()
                .put("/api/membres/{id}/promouvoir-admin-organisation")
                .then()
                .statusCode(200)
                .body("id", notNullValue());
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("PUT /api/membres/{id}/promouvoir-admin-organisation retourne 200 même si Keycloak échoue (non bloquant, couvre L765-766)")
    void promouvoirAdminOrganisation_keycloakFails_returns200() {
        UUID membreId = UUID.randomUUID();
        Membre membrePromu = new Membre();
        membrePromu.setId(membreId);
        membrePromu.setEmail("promo-fail@test.com");

        MembreResponse response = new MembreResponse();
        response.setId(membreId);

        when(membreService.promouvoirAdminOrganisation(membreId)).thenReturn(membrePromu);
        when(membreService.convertToResponse(membrePromu)).thenReturn(response);
        doThrow(new RuntimeException("Keycloak unreachable"))
                .when(keycloakSyncService).promouvoirAdminOrganisationDansKeycloak(membreId);

        given()
                .pathParam("id", membreId)
                .when()
                .put("/api/membres/{id}/promouvoir-admin-organisation")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // Branches manquantes : L181 isEmpty(), L204-205 isBlank(), L219 keycloakId != null
    // ============================================================

    @Test
    @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
    @DisplayName("GET /api/membres/me — roles non-null mais vide → isEmpty() true → enrichi depuis Keycloak (branche isEmpty L181)")
    void obtenirMembreConnecte_rolesListeVideNonNull_enrichiDepuisKeycloak_returns200() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(membre.getId());
        response.setRoles(java.util.Collections.emptyList()); // non-null mais vide → isEmpty() = true

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.of(membre));
        when(membreService.convertToResponse(membre)).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "provuser2@test.com", roles = {"USER"})
    @DisplayName("GET /api/membres/me — given_name/family_name non-blank → prenom/nom mis à jour (assignment instructions L204-205)")
    void obtenirMembreConnecte_givenFamilyNameNonBlank_setsPrenom_returns200() {
        when(jwt.getClaim("given_name")).thenReturn("Jean"); // non-null non-blank → L204 true → prenom = givenName EXECUTED
        when(jwt.getClaim("family_name")).thenReturn("Dupont"); // non-null non-blank → L205 true → nom = familyName EXECUTED

        Membre nouveau = new Membre();
        nouveau.setId(UUID.randomUUID());
        nouveau.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(nouveau.getId());
        response.setRoles(List.of("USER"));

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.empty());
        when(membreService.convertFromCreateRequest(any())).thenReturn(nouveau);
        when(membreService.creerMembre(any())).thenReturn(nouveau);
        when(membreService.activerMembre(any())).thenReturn(nouveau);
        when(membreService.convertToResponse(any())).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "membre@test.com", roles = {"default-roles-unionflow", "MEMBRE"})
    @DisplayName("GET /api/membres/me — rôle 'default-roles-*' → filtré par lambda → couvre branche startsWith L186")
    void obtenirMembreConnecte_roleDefaultRoles_filteredOut_returns200() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(membre.getId());
        response.setRoles(null); // null → enter filtering block at L181

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.of(membre));
        when(membreService.convertToResponse(membre)).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "provision@test.com", roles = {"USER"})
    @DisplayName("GET /api/membres/me — given_name/family_name blanc → prenom/nom non mis à jour (branches isBlank false L204-205) + sub UUID valide → keycloakId non-null (L219)")
    void obtenirMembreConnecte_givenFamilyNameBlank_subUuidValide_returns200() {
        when(jwt.getClaim("given_name")).thenReturn("   "); // non-null mais blank → L204 false
        when(jwt.getClaim("family_name")).thenReturn("   "); // non-null mais blank → L205 false
        when(jwt.getSubject()).thenReturn("00000000-0000-0000-0000-000000000099"); // UUID valide → keycloakId != null → L219 true

        Membre nouveau = new Membre();
        nouveau.setId(UUID.randomUUID());
        nouveau.setActif(true);

        MembreResponse response = new MembreResponse();
        response.setId(nouveau.getId());
        response.setRoles(List.of("USER"));

        when(membreService.trouverParEmail(anyString())).thenReturn(Optional.empty());
        when(membreService.convertFromCreateRequest(any())).thenReturn(nouveau);
        when(membreService.creerMembre(any())).thenReturn(nouveau);
        when(membreService.activerMembre(any())).thenReturn(nouveau);
        when(membreService.convertToResponse(any())).thenReturn(response);

        given()
                .when()
                .get("/api/membres/me")
                .then()
                .statusCode(200);
    }

    // ============================================================
    // reinitialiserMotDePasse — PUT /{id}/reinitialiser-mot-de-passe
    // ============================================================

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("PUT /api/membres/{id}/reinitialiser-mot-de-passe retourne 200 avec motDePasseTemporaire")
    void reinitialiserMotDePasse_success_returns200WithPassword() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("reset@test.com");

        MembreResponse response = new MembreResponse();
        response.setId(membreId);

        when(membreService.trouverParId(membreId)).thenReturn(Optional.of(membre));
        when(keycloakSyncService.reinitialiserMotDePasse(membreId)).thenReturn("NewP@ss1234");
        when(membreService.convertToResponse(membre)).thenReturn(response);

        given()
                .pathParam("id", membreId)
                .when()
                .put("/api/membres/{id}/reinitialiser-mot-de-passe")
                .then()
                .statusCode(200)
                .body("motDePasseTemporaire", equalTo("NewP@ss1234"));
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("PUT /api/membres/{id}/reinitialiser-mot-de-passe retourne 404 si membre non trouvé")
    void reinitialiserMotDePasse_notFound_returns404() {
        UUID membreId = UUID.randomUUID();
        when(membreService.trouverParId(membreId)).thenReturn(Optional.empty());

        given()
                .pathParam("id", membreId)
                .when()
                .put("/api/membres/{id}/reinitialiser-mot-de-passe")
                .then()
                .statusCode(404);
    }

    @Test
    @TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
    @DisplayName("PUT /api/membres/{id}/reinitialiser-mot-de-passe retourne 400 si membre sans compte Keycloak")
    void reinitialiserMotDePasse_noKeycloakAccount_returns400() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("nokeycloak@test.com");

        when(membreService.trouverParId(membreId)).thenReturn(Optional.of(membre));
        when(keycloakSyncService.reinitialiserMotDePasse(membreId))
                .thenThrow(new IllegalStateException("Le membre n'a pas de compte Keycloak"));

        given()
                .pathParam("id", membreId)
                .when()
                .put("/api/membres/{id}/reinitialiser-mot-de-passe")
                .then()
                .statusCode(400);
    }

    @Test
    @TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
    @DisplayName("PUT /api/membres/{id}/reinitialiser-mot-de-passe accessible à ADMIN_ORGANISATION")
    void reinitialiserMotDePasse_adminOrganisation_returns200() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("orgmembre@test.com");

        MembreResponse response = new MembreResponse();
        response.setId(membreId);

        when(membreService.trouverParId(membreId)).thenReturn(Optional.of(membre));
        when(keycloakSyncService.reinitialiserMotDePasse(membreId)).thenReturn("TempPass!99");
        when(membreService.convertToResponse(membre)).thenReturn(response);

        given()
                .pathParam("id", membreId)
                .when()
                .put("/api/membres/{id}/reinitialiser-mot-de-passe")
                .then()
                .statusCode(200);
    }

    @Test
    @TestSecurity(user = "user@test.com", roles = {"USER"})
    @DisplayName("PUT /api/membres/{id}/reinitialiser-mot-de-passe interdit pour USER (403)")
    void reinitialiserMotDePasse_userRole_returns403() {
        UUID membreId = UUID.randomUUID();

        given()
                .pathParam("id", membreId)
                .when()
                .put("/api/membres/{id}/reinitialiser-mot-de-passe")
                .then()
                .statusCode(403);
    }
}
