package dev.lions.unionflow.server.service;

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

import org.mockito.ArgumentCaptor;

import dev.lions.unionflow.server.client.AdminRoleServiceClient;
import dev.lions.unionflow.server.client.AdminUserServiceClient;
import dev.lions.unionflow.server.client.RoleServiceClient;
import dev.lions.unionflow.server.client.UserServiceClient;
import dev.lions.user.manager.dto.user.PasswordResetRequestDTO;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.user.manager.dto.user.UserDTO;
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@QuarkusTest
class MembreKeycloakSyncServiceTest {

    @Inject
    MembreKeycloakSyncService syncService;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    @RestClient
    UserServiceClient userServiceClient;

    @InjectMock
    @RestClient
    RoleServiceClient roleServiceClient;

    @InjectMock
    @RestClient
    AdminUserServiceClient adminUserServiceClient; // used by MembreKeycloakSyncService

    @InjectMock
    @RestClient
    AdminRoleServiceClient adminRoleServiceClient; // used by MembreKeycloakSyncService

    // =========================================================================
    // provisionKeycloakUser
    // =========================================================================

    @Test
    @DisplayName("provisionKeycloakUser échoue si le membre n'existe pas")
    void provisionKeycloakUser_failsIfMembreNotFound() {
        UUID membreId = UUID.randomUUID();
        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> syncService.provisionKeycloakUser(membreId))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("provisionKeycloakUser échoue si le membre a déjà un ID Keycloak")
    void provisionKeycloakUser_failsIfAlreadyLinked() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(UUID.randomUUID());

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        assertThatThrownBy(() -> syncService.provisionKeycloakUser(membreId))
                .isInstanceOf(IllegalStateException.class);
    }

    @Test
    @DisplayName("provisionKeycloakUser crée un utilisateur Keycloak et lie le membre")
    void provisionKeycloakUser_createsAndLinks() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("test@unionflow.dev");
        membre.setNom("Doe");
        membre.setPrenom("John");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        syncService.provisionKeycloakUser(membreId);

        ArgumentCaptor<UserDTO> userCaptor = ArgumentCaptor.forClass(UserDTO.class);
        verify(adminUserServiceClient).createUser(userCaptor.capture(), eq("unionflow"));
        // Le username doit être la partie locale de l'email (sans @)
        assertThat(userCaptor.getValue().getUsername()).isEqualTo("test");
        assertThat(userCaptor.getValue().getUsername()).doesNotContain("@");
        verify(membreRepository).persist(membre);
        verify(adminUserServiceClient).sendVerificationEmail(eq(createdUser.getId()), eq("unionflow"));
    }

    @Test
    @DisplayName("provisionKeycloakUser: username sanitisé (+ remplacé par _) depuis la partie locale de l'email")
    void provisionKeycloakUser_usernameIsSanitizedEmailLocalPart() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("jean+test@gmail.com"); // + hors-pattern → doit devenir jean_test
        membre.setNom("Jean");
        membre.setPrenom("Test");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        syncService.provisionKeycloakUser(membreId);

        ArgumentCaptor<UserDTO> captor = ArgumentCaptor.forClass(UserDTO.class);
        verify(adminUserServiceClient).createUser(captor.capture(), anyString());
        assertThat(captor.getValue().getUsername())
                .doesNotContain("@")
                .matches("[a-zA-Z0-9._-]+")
                .isEqualTo("jean_test");
    }

    @Test
    @DisplayName("provisionKeycloakUser: username complété à 3 chars minimum si partie locale trop courte")
    void provisionKeycloakUser_usernameMinLength() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("ab@test.com"); // partie locale "ab" (2 chars) → doit devenir "ab_uf"
        membre.setNom("Ab");
        membre.setPrenom("Test");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        syncService.provisionKeycloakUser(membreId);

        ArgumentCaptor<UserDTO> captor = ArgumentCaptor.forClass(UserDTO.class);
        verify(adminUserServiceClient).createUser(captor.capture(), anyString());
        assertThat(captor.getValue().getUsername())
                .hasSizeGreaterThanOrEqualTo(3)
                .isEqualTo("ab_uf");
    }

    @Test
    @DisplayName("provisionKeycloakUser lève IllegalStateException si un user Keycloak existe déjà avec cet email")
    void provisionKeycloakUser_failsIfKeycloakUserAlreadyExists() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("duplicate@unionflow.dev");
        membre.setNom("Dupont");
        membre.setPrenom("Pierre");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserDTO existingUser = new UserDTO();
        existingUser.setId(UUID.randomUUID().toString());
        existingUser.setEmail("duplicate@unionflow.dev");

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.singletonList(existingUser));
        when(adminUserServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult);

        assertThatThrownBy(() -> syncService.provisionKeycloakUser(membreId))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("duplicate@unionflow.dev");
    }

    @Test
    @DisplayName("provisionKeycloakUser continue si la recherche Keycloak lève une exception non-ISE")
    void provisionKeycloakUser_continuesOnSearchException() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("search-fail@unionflow.dev");
        membre.setNom("Search");
        membre.setPrenom("Fail");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        // search throws a non-ISE exception
        when(adminUserServiceClient.searchUsers(any())).thenThrow(new RuntimeException("Service unavailable"));

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        // Should not throw — it logs warning and continues
        syncService.provisionKeycloakUser(membreId);

        verify(adminUserServiceClient).createUser(any(UserDTO.class), eq("unionflow"));
    }

    @Test
    @DisplayName("provisionKeycloakUser tolère un ID Keycloak invalide (non-UUID) dans la réponse")
    void provisionKeycloakUser_toleratesInvalidKeycloakId() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("invalid-id@unionflow.dev");
        membre.setNom("Invalid");
        membre.setPrenom("Id");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any())).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId("not-a-valid-uuid!!!"); // invalid UUID
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        // Should not throw — logs warning and persists with null keycloakId
        syncService.provisionKeycloakUser(membreId);

        verify(membreRepository).persist(membre);
    }

    @Test
    @DisplayName("provisionKeycloakUser lève RuntimeException si la création Keycloak échoue")
    void provisionKeycloakUser_throwsRuntimeExceptionOnCreateFailure() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("create-fail@unionflow.dev");
        membre.setNom("Create");
        membre.setPrenom("Fail");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any())).thenReturn(searchResult);
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString()))
                .thenThrow(new RuntimeException("Keycloak create failed"));

        assertThatThrownBy(() -> syncService.provisionKeycloakUser(membreId))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Impossible de créer le compte Keycloak");
    }

    @Test
    @DisplayName("provisionKeycloakUser tolère l'échec d'envoi de l'email de vérification")
    void provisionKeycloakUser_toleratesVerificationEmailFailure() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("email-fail@unionflow.dev");
        membre.setNom("Email");
        membre.setPrenom("Fail");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any())).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);
        doThrow(new RuntimeException("SMTP unavailable"))
                .when(adminUserServiceClient).sendVerificationEmail(anyString(), anyString());

        // Should not throw — email failure is non-blocking
        syncService.provisionKeycloakUser(membreId);

        verify(membreRepository).persist(membre);
    }

    // =========================================================================
    // promouvoirAdminOrganisationDansKeycloak
    // =========================================================================

    @Test
    @DisplayName("promouvoirAdminOrganisation échoue si le membre n'existe pas")
    void promouvoirAdminOrganisation_failsIfMembreNotFound() {
        UUID membreId = UUID.randomUUID();
        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> syncService.promouvoirAdminOrganisationDansKeycloak(membreId))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("promouvoirAdminOrganisation assigne ADMIN_ORGANISATION et révoque MEMBRE/MEMBRE_ACTIF")
    void promouvoirAdminOrganisation_assignsAdminRoleAndRevokesMemberRoles() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("admin@unionflow.dev");
        membre.setNom("Admin");
        membre.setPrenom("New");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserDTO user = new UserDTO();
        user.setId(keycloakId.toString());
        user.setEnabled(true);
        user.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(eq(keycloakId.toString()), eq("unionflow"))).thenReturn(user);
        doNothing().when(adminRoleServiceClient).revokeRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));
        doNothing().when(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));

        syncService.promouvoirAdminOrganisationDansKeycloak(membreId);

        verify(adminRoleServiceClient).revokeRealmRoles(eq(keycloakId.toString()), eq("unionflow"), any(RoleServiceClient.RoleNamesRequest.class));
        verify(adminRoleServiceClient).assignRealmRoles(eq(keycloakId.toString()), eq("unionflow"), any(RoleServiceClient.RoleNamesRequest.class));
    }

    @Test
    @DisplayName("promouvoirAdminOrganisation active le compte s'il est désactivé dans Keycloak")
    void promouvoirAdminOrganisation_enablesDisabledAccount() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("disabled-admin@unionflow.dev");
        membre.setNom("Disabled");
        membre.setPrenom("Admin");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserDTO user = new UserDTO();
        user.setId(keycloakId.toString());
        user.setEnabled(false);
        user.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(anyString(), anyString())).thenReturn(user);
        when(adminUserServiceClient.updateUser(anyString(), any(UserDTO.class), anyString())).thenReturn(user);
        doNothing().when(adminRoleServiceClient).revokeRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));
        doNothing().when(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));

        syncService.promouvoirAdminOrganisationDansKeycloak(membreId);

        assertThat(user.getEnabled()).isTrue();
        verify(adminUserServiceClient).updateUser(anyString(), any(UserDTO.class), anyString());
    }

    @Test
    @DisplayName("promouvoirAdminOrganisation provisionne Keycloak si keycloakId est null")
    void promouvoirAdminOrganisation_provisionesIfNoKeycloakAccount() {
        UUID membreId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("no-kc-admin@unionflow.dev");
        membre.setNom("No");
        membre.setPrenom("KC");
        // keycloakId == null

        UUID newKeycloakId = UUID.randomUUID();
        Membre membreWithKc = new Membre();
        membreWithKc.setId(membreId);
        membreWithKc.setEmail("no-kc-admin@unionflow.dev");
        membreWithKc.setNom("No");
        membreWithKc.setPrenom("KC");
        membreWithKc.setKeycloakId(newKeycloakId);

        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre))       // 1er appel (promouvoir) : pas de keycloakId → déclenche provisionnement
                .thenReturn(Optional.of(membre))       // 2e appel (provisionKeycloakUser interne) : pas de keycloakId
                .thenReturn(Optional.of(membreWithKc)); // 3e appel : rechargement après provisionnement

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(java.util.Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any())).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(newKeycloakId.toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        UserDTO fetchedUser = new UserDTO();
        fetchedUser.setId(newKeycloakId.toString());
        fetchedUser.setEnabled(true);
        fetchedUser.setRealmRoles(new java.util.ArrayList<>());
        fetchedUser.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(eq(newKeycloakId.toString()), anyString())).thenReturn(fetchedUser);
        when(adminUserServiceClient.updateUser(anyString(), any(UserDTO.class), anyString())).thenReturn(fetchedUser);
        doNothing().when(adminRoleServiceClient).revokeRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));
        doNothing().when(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));

        syncService.promouvoirAdminOrganisationDansKeycloak(membreId);

        verify(adminUserServiceClient).createUser(any(UserDTO.class), eq("unionflow"));
        verify(adminRoleServiceClient).assignRealmRoles(eq(newKeycloakId.toString()), eq("unionflow"), any(RoleServiceClient.RoleNamesRequest.class));
    }

    @Test
    @DisplayName("promouvoirAdminOrganisation lève NotFoundException si membre introuvable après provisionnement (couvre lambda L231)")
    void promouvoirAdminOrganisation_membreIntrouvabledApresProvisionnement() {
        UUID membreId = UUID.randomUUID();

        Membre membreSansKc = new Membre();
        membreSansKc.setId(membreId);
        membreSansKc.setEmail("ghost-promo@unionflow.dev");
        membreSansKc.setNom("Ghost");
        membreSansKc.setPrenom("Promo");
        // keycloakId == null → déclenche provisionnement

        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membreSansKc))   // 1er: promouvoirAdminOrganisationDansKeycloak
                .thenReturn(Optional.of(membreSansKc))   // 2e: dans provisionKeycloakUser
                .thenReturn(Optional.empty());            // 3e: rechargement → lambda orElseThrow

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any())).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        assertThatThrownBy(() -> syncService.promouvoirAdminOrganisationDansKeycloak(membreId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé après provisionnement");
    }

    @Test
    @DisplayName("promouvoirAdminOrganisation tolère l'échec de révocation MEMBRE/MEMBRE_ACTIF (couvre L252-253)")
    void promouvoirAdminOrganisation_toleratesRevokeRolesFailure() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("revoke-fail@unionflow.dev");
        membre.setNom("Revoke");
        membre.setPrenom("Fail");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserDTO user = new UserDTO();
        user.setId(keycloakId.toString());
        user.setEnabled(true);
        user.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(anyString(), anyString())).thenReturn(user);

        // Révocation non bloquante — lève une exception qui doit être absorbée
        doThrow(new RuntimeException("Role not found"))
                .when(adminRoleServiceClient).revokeRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));
        doNothing().when(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));

        // Ne doit pas lever d'exception
        syncService.promouvoirAdminOrganisationDansKeycloak(membreId);

        verify(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));
    }

    @Test
    @DisplayName("promouvoirAdminOrganisation lève RuntimeException si l'appel Keycloak échoue")
    void promouvoirAdminOrganisation_throwsRuntimeExceptionOnKeycloakFailure() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("fail-admin@unionflow.dev");
        membre.setNom("Fail");
        membre.setPrenom("Admin");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        when(adminUserServiceClient.getUserById(anyString(), anyString()))
                .thenThrow(new RuntimeException("Keycloak unreachable"));

        assertThatThrownBy(() -> syncService.promouvoirAdminOrganisationDansKeycloak(membreId))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Impossible de promouvoir le compte Keycloak");
    }

    // =========================================================================
    // activerMembreDansKeycloak
    // =========================================================================

    @Test
    @DisplayName("activerMembreDansKeycloak échoue si le membre n'existe pas")
    void activerMembreDansKeycloak_failsIfMembreNotFound() {
        UUID membreId = UUID.randomUUID();
        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> syncService.activerMembreDansKeycloak(membreId))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("activerMembreDansKeycloak assigne MEMBRE_ACTIF via roleServiceClient")
    void activerMembreDansKeycloak_assignsMemberActifRole() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("membre@unionflow.dev");
        membre.setNom("Membre");
        membre.setPrenom("Test");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserDTO user = new UserDTO();
        user.setId(keycloakId.toString());
        user.setEnabled(true);
        user.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(eq(keycloakId.toString()), eq("unionflow"))).thenReturn(user);
        doNothing().when(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));

        syncService.activerMembreDansKeycloak(membreId);

        verify(adminRoleServiceClient).assignRealmRoles(
                eq(keycloakId.toString()), eq("unionflow"), any(RoleServiceClient.RoleNamesRequest.class));
    }

    @Test
    @DisplayName("activerMembreDansKeycloak provisionne Keycloak si keycloakId est null (couvre L174-178)")
    void activerMembreDansKeycloak_provisionesIfNoKeycloakAccount() {
        UUID membreId = UUID.randomUUID();
        UUID newKeycloakId = UUID.randomUUID();

        Membre membreSansKc = new Membre();
        membreSansKc.setId(membreId);
        membreSansKc.setEmail("no-kc-activ@unionflow.dev");
        membreSansKc.setNom("NoKc");
        membreSansKc.setPrenom("Activ");
        // keycloakId == null → déclenche le provisionnement

        Membre membreAvecKc = new Membre();
        membreAvecKc.setId(membreId);
        membreAvecKc.setEmail("no-kc-activ@unionflow.dev");
        membreAvecKc.setNom("NoKc");
        membreAvecKc.setPrenom("Activ");
        membreAvecKc.setKeycloakId(newKeycloakId);

        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membreSansKc))   // 1er appel: activerMembreDansKeycloak voit null keycloakId
                .thenReturn(Optional.of(membreSansKc))   // 2e appel: dans provisionKeycloakUser
                .thenReturn(Optional.of(membreAvecKc));  // 3e appel: rechargement après provisionnement

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any())).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(newKeycloakId.toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        UserDTO fetchedUser = new UserDTO();
        fetchedUser.setId(newKeycloakId.toString());
        fetchedUser.setEnabled(true);
        fetchedUser.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(eq(newKeycloakId.toString()), anyString())).thenReturn(fetchedUser);
        doNothing().when(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));

        syncService.activerMembreDansKeycloak(membreId);

        verify(adminUserServiceClient).createUser(any(UserDTO.class), eq("unionflow"));
        verify(adminRoleServiceClient).assignRealmRoles(eq(newKeycloakId.toString()), eq("unionflow"), any(RoleServiceClient.RoleNamesRequest.class));
    }

    @Test
    @DisplayName("activerMembreDansKeycloak lève NotFoundException si membre introuvable après provisionnement (couvre lambda L178)")
    void activerMembreDansKeycloak_membreIntrouvabledApresProvisionnement() {
        UUID membreId = UUID.randomUUID();

        Membre membreSansKc = new Membre();
        membreSansKc.setId(membreId);
        membreSansKc.setEmail("ghost-activ@unionflow.dev");
        membreSansKc.setNom("Ghost");
        membreSansKc.setPrenom("Activ");
        // keycloakId == null → déclenche provisionnement

        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membreSansKc))   // 1er: activerMembreDansKeycloak
                .thenReturn(Optional.of(membreSansKc))   // 2e: dans provisionKeycloakUser
                .thenReturn(Optional.empty());            // 3e: rechargement → lambda orElseThrow

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any())).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        assertThatThrownBy(() -> syncService.activerMembreDansKeycloak(membreId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé après provisionnement");
    }

    @Test
    @DisplayName("activerMembreDansKeycloak lève RuntimeException si l'appel Keycloak échoue (couvre L202-204)")
    void activerMembreDansKeycloak_throwsRuntimeExceptionOnKeycloakFailure() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("kc-fail@unionflow.dev");
        membre.setNom("Fail");
        membre.setPrenom("Activ");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        when(adminUserServiceClient.getUserById(anyString(), anyString()))
                .thenThrow(new RuntimeException("Keycloak unreachable"));

        assertThatThrownBy(() -> syncService.activerMembreDansKeycloak(membreId))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Impossible d'activer le compte Keycloak");
    }

    @Test
    @DisplayName("activerMembreDansKeycloak active le compte s'il est désactivé")
    void activerMembreDansKeycloak_enablesDisabledAccount() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("disabled@unionflow.dev");
        membre.setNom("Disabled");
        membre.setPrenom("Membre");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserDTO user = new UserDTO();
        user.setId(keycloakId.toString());
        user.setEnabled(false);
        user.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(anyString(), anyString())).thenReturn(user);
        when(adminUserServiceClient.updateUser(anyString(), any(UserDTO.class), anyString())).thenReturn(user);
        doNothing().when(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));

        syncService.activerMembreDansKeycloak(membreId);

        assertThat(user.getEnabled()).isTrue();
        verify(adminUserServiceClient).updateUser(anyString(), any(UserDTO.class), anyString());
        verify(adminRoleServiceClient).assignRealmRoles(anyString(), anyString(), any(RoleServiceClient.RoleNamesRequest.class));
    }

    // =========================================================================
    // syncMembreToKeycloak
    // =========================================================================

    @Test
    @DisplayName("syncMembreToKeycloak lève NotFoundException si le membre n'existe pas")
    void syncMembreToKeycloak_failsIfMembreNotFound() {
        UUID membreId = UUID.randomUUID();
        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> syncService.syncMembreToKeycloak(membreId))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("syncMembreToKeycloak provisionne automatiquement si pas de compte Keycloak")
    void syncMembreToKeycloak_provisionesIfNoKeycloakAccount() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("no-kc@unionflow.dev");
        membre.setNom("No");
        membre.setPrenom("KC");
        // keycloakId == null → doit appeler provisionKeycloakUser

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any())).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        syncService.syncMembreToKeycloak(membreId);

        // provisionKeycloakUser was invoked internally
        verify(adminUserServiceClient).createUser(any(UserDTO.class), eq("unionflow"));
    }

    @Test
    @DisplayName("syncMembreToKeycloak met à jour le user Keycloak si le membre est déjà lié")
    void syncMembreToKeycloak_updatesExistingKeycloakUser() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("sync@unionflow.dev");
        membre.setNom("Sync");
        membre.setPrenom("Test");
        membre.setActif(true);

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserDTO remoteUser = new UserDTO();
        remoteUser.setId(keycloakId.toString());
        remoteUser.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(eq(keycloakId.toString()), eq("unionflow")))
                .thenReturn(remoteUser);

        syncService.syncMembreToKeycloak(membreId);

        verify(adminUserServiceClient).updateUser(eq(keycloakId.toString()), any(UserDTO.class), eq("unionflow"));
    }

    @Test
    @DisplayName("syncMembreToKeycloak lève RuntimeException si l'appel getUserById échoue")
    void syncMembreToKeycloak_throwsRuntimeExceptionOnGetUserFailure() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("get-fail@unionflow.dev");
        membre.setNom("Get");
        membre.setPrenom("Fail");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        when(adminUserServiceClient.getUserById(anyString(), anyString()))
                .thenThrow(new RuntimeException("Keycloak unreachable"));

        assertThatThrownBy(() -> syncService.syncMembreToKeycloak(membreId))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Impossible de synchroniser le user Keycloak");
    }

    @Test
    @DisplayName("syncMembreToKeycloak utilise actif=true par défaut si membre.actif est null")
    void syncMembreToKeycloak_usesDefaultEnabledWhenActifIsNull() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("null-actif@unionflow.dev");
        membre.setNom("Null");
        membre.setPrenom("Actif");
        membre.setActif(null); // null → defaults to true

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserDTO remoteUser = new UserDTO();
        remoteUser.setId(keycloakId.toString());
        remoteUser.setRealmName("unionflow");
        when(adminUserServiceClient.getUserById(anyString(), anyString())).thenReturn(remoteUser);

        syncService.syncMembreToKeycloak(membreId);

        verify(adminUserServiceClient).updateUser(anyString(), any(UserDTO.class), anyString());
    }

    // =========================================================================
    // syncKeycloakToMembre
    // =========================================================================

    @Test
    @DisplayName("syncKeycloakToMembre ne fait rien si aucun Membre n'est lié au user Keycloak")
    void syncKeycloakToMembre_doesNothingIfNoMembreMapped() {
        String keycloakUserId = UUID.randomUUID().toString();
        when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.empty());

        syncService.syncKeycloakToMembre(keycloakUserId, "unionflow");

        verifyNoInteractions(adminUserServiceClient);
    }

    @Test
    @DisplayName("syncKeycloakToMembre met à jour le Membre avec les données Keycloak")
    void syncKeycloakToMembre_updatesMembreFromKeycloak() {
        String keycloakUserId = UUID.randomUUID().toString();

        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setEmail("old@unionflow.dev");
        membre.setNom("OldNom");
        membre.setPrenom("OldPrenom");

        when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.of(membre));

        UserDTO keycloakUser = new UserDTO();
        keycloakUser.setId(keycloakUserId);
        keycloakUser.setPrenom("NewPrenom");
        keycloakUser.setNom("NewNom");
        keycloakUser.setEmail("new@unionflow.dev");
        keycloakUser.setEnabled(false);

        when(adminUserServiceClient.getUserById(eq(keycloakUserId), eq("unionflow"))).thenReturn(keycloakUser);

        syncService.syncKeycloakToMembre(keycloakUserId, "unionflow");

        assertThat(membre.getPrenom()).isEqualTo("NewPrenom");
        assertThat(membre.getNom()).isEqualTo("NewNom");
        assertThat(membre.getEmail()).isEqualTo("new@unionflow.dev");
        assertThat(membre.getActif()).isFalse();
        verify(membreRepository).persist(membre);
    }

    @Test
    @DisplayName("syncKeycloakToMembre utilise DEFAULT_REALM si realm est null")
    void syncKeycloakToMembre_usesDefaultRealmWhenNull() {
        String keycloakUserId = UUID.randomUUID().toString();

        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.of(membre));

        UserDTO keycloakUser = new UserDTO();
        keycloakUser.setId(keycloakUserId);
        keycloakUser.setPrenom("P");
        keycloakUser.setNom("N");
        keycloakUser.setEmail("e@e.com");
        keycloakUser.setEnabled(true);

        when(adminUserServiceClient.getUserById(eq(keycloakUserId), eq("unionflow"))).thenReturn(keycloakUser);

        syncService.syncKeycloakToMembre(keycloakUserId, null); // null realm

        verify(adminUserServiceClient).getUserById(eq(keycloakUserId), eq("unionflow"));
    }

    @Test
    @DisplayName("syncKeycloakToMembre lève RuntimeException si getUserById échoue")
    void syncKeycloakToMembre_throwsRuntimeExceptionOnFailure() {
        String keycloakUserId = UUID.randomUUID().toString();

        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.of(membre));
        when(adminUserServiceClient.getUserById(anyString(), anyString()))
                .thenThrow(new RuntimeException("Timeout"));

        assertThatThrownBy(() -> syncService.syncKeycloakToMembre(keycloakUserId, "unionflow"))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Impossible de synchroniser depuis Keycloak");
    }

    // =========================================================================
    // findMembreByKeycloakUserId
    // =========================================================================

    @Test
    @DisplayName("findMembreByKeycloakUserId retourne Optional.empty() si aucun membre n'est lié")
    void findMembreByKeycloakUserId_returnsEmptyIfNotFound() {
        String keycloakUserId = UUID.randomUUID().toString();
        when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.empty());

        Optional<Membre> result = syncService.findMembreByKeycloakUserId(keycloakUserId);

        assertThat(result).isEmpty();
    }

    @Test
    @DisplayName("findMembreByKeycloakUserId retourne le Membre lié si trouvé")
    void findMembreByKeycloakUserId_returnsMembre() {
        String keycloakUserId = UUID.randomUUID().toString();
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setEmail("linked@unionflow.dev");

        when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.of(membre));

        Optional<Membre> result = syncService.findMembreByKeycloakUserId(keycloakUserId);

        assertThat(result).isPresent();
        assertThat(result.get()).isSameAs(membre);
    }

    // =========================================================================
    // unlinkKeycloakUser
    // =========================================================================

    @Test
    @DisplayName("unlinkKeycloakUser lève NotFoundException si le membre n'existe pas")
    void unlinkKeycloakUser_failsIfMembreNotFound() {
        UUID membreId = UUID.randomUUID();
        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> syncService.unlinkKeycloakUser(membreId))
                .isInstanceOf(NotFoundException.class);
    }

    // =========================================================================
    // provisionKeycloakUser — branche searchResult == null (L97 branche null)
    // =========================================================================

    @Test
    @DisplayName("provisionKeycloakUser avec searchResult==null depuis API → continue (branche null L97)")
    void provisionKeycloakUser_searchResultNull_continues() {
        UUID membreId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setNom("Null");
        membre.setPrenom("Search");
        membre.setEmail("null-search-" + membreId + "@test.com");
        membre.setKeycloakId(null);

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        // searchResult == null → condition L97 = false → continue
        when(adminUserServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(null);
        // provisionKeycloakUser tente ensuite de créer l'utilisateur via createUser
        // On mock createUser pour qu'il retourne un UserDTO valide
        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        createdUser.setUsername(membre.getEmail());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        // Ne doit pas lever d'exception (searchResult null → condition false → continue)
        org.assertj.core.api.Assertions.assertThatCode(() -> syncService.provisionKeycloakUser(membreId))
                .doesNotThrowAnyException();
    }

    @Test
    @DisplayName("provisionKeycloakUser avec searchResult.getUsers()==null → continue (branche getUsers()==null L97)")
    void provisionKeycloakUser_usersNull_continues() {
        UUID membreId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setNom("UsersNull");
        membre.setPrenom("Test");
        membre.setEmail("users-null-" + membreId + "@test.com");
        membre.setKeycloakId(null);

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        // searchResult non-null mais users == null → condition L97 = false → continue
        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(null);
        when(adminUserServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        createdUser.setUsername(membre.getEmail());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        org.assertj.core.api.Assertions.assertThatCode(() -> syncService.provisionKeycloakUser(membreId))
                .doesNotThrowAnyException();
    }

    // =========================================================================
    // L117: createdUser.getId() == null → false branch (skip setKeycloakId)
    // =========================================================================

    @Test
    @DisplayName("provisionKeycloakUser avec createdUser.getId() null → L117 false → keycloakId non settée")
    void provisionKeycloakUser_createdUserIdNull_L117False_keycloakIdNotSet() {
        UUID membreId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setNom("IdNull");
        membre.setPrenom("Test");
        membre.setEmail("id-null-" + membreId + "@test.com");
        membre.setKeycloakId(null);

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        // searchResult null ou sans users → membre not found → create
        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(null);
        when(adminUserServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult);

        // createdUser with getId() = null → L117: null != null = false → skip setKeycloakId
        UserDTO createdUser = new UserDTO();
        createdUser.setId(null); // null → L117 false branch
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        org.assertj.core.api.Assertions.assertThatCode(() -> syncService.provisionKeycloakUser(membreId))
                .doesNotThrowAnyException();

        // keycloakId should remain null since getId() was null
        assertThat(membre.getKeycloakId()).isNull();
    }

    @Test
    @DisplayName("unlinkKeycloakUser ne fait rien si le membre n'a pas de compte Keycloak lié")
    void unlinkKeycloakUser_doesNothingIfNoKeycloakId() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(null);

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        syncService.unlinkKeycloakUser(membreId);

        // persist should not be called when there is nothing to unlink
        verify(membreRepository, never()).persist(any(Membre.class));
    }

    @Test
    @DisplayName("unlinkKeycloakUser supprime le lien Keycloak et persiste le membre")
    void unlinkKeycloakUser_removesKeycloakLink() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setNom("Unlink");
        membre.setPrenom("Test");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        syncService.unlinkKeycloakUser(membreId);

        assertThat(membre.getKeycloakId()).isNull();
        verify(membreRepository).persist(membre);
    }

    @Test
    @DisplayName("provisionKeycloakUser: email sans '@' → emailLocalPart = email complet (branche false L407)")
    void provisionKeycloakUser_emailSansArobe_usesEmailEntier() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("noemailatsign"); // pas de '@' → L407: false branch → emailLocalPart = membre.getEmail()
        membre.setNom("Doe");
        membre.setPrenom("John");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        UserSearchResultDTO searchResult = new UserSearchResultDTO();
        searchResult.setUsers(Collections.emptyList());
        when(adminUserServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult);

        UserDTO createdUser = new UserDTO();
        createdUser.setId(UUID.randomUUID().toString());
        when(adminUserServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);

        syncService.provisionKeycloakUser(membreId);

        ArgumentCaptor<UserDTO> userCaptor = ArgumentCaptor.forClass(UserDTO.class);
        verify(adminUserServiceClient).createUser(userCaptor.capture(), eq("unionflow"));
        assertThat(userCaptor.getValue().getUsername()).isEqualTo("noemailatsign");
    }

    // =========================================================================
    // reinitialiserMotDePasse
    // =========================================================================

    @Test
    @DisplayName("reinitialiserMotDePasse lève NotFoundException si le membre n'existe pas")
    void reinitialiserMotDePasse_failsIfMembreNotFound() {
        UUID membreId = UUID.randomUUID();
        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> syncService.reinitialiserMotDePasse(membreId))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("reinitialiserMotDePasse lève IllegalStateException si le membre n'a pas de compte Keycloak")
    void reinitialiserMotDePasse_failsIfNoKeycloakId() {
        UUID membreId = UUID.randomUUID();
        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("nokeycloak@test.com");
        // keycloakId == null

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));

        assertThatThrownBy(() -> syncService.reinitialiserMotDePasse(membreId))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("compte Keycloak");
    }

    @Test
    @DisplayName("reinitialiserMotDePasse appelle adminUserServiceClient.resetPassword avec le bon userId et realm")
    void reinitialiserMotDePasse_callsResetPasswordWithCorrectParams() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("membre@test.com");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        doNothing().when(adminUserServiceClient).resetPassword(anyString(), anyString(), any(PasswordResetRequestDTO.class));

        syncService.reinitialiserMotDePasse(membreId);

        verify(adminUserServiceClient).resetPassword(
                eq(keycloakId.toString()),
                eq("unionflow"),
                any(PasswordResetRequestDTO.class));
    }

    @Test
    @DisplayName("reinitialiserMotDePasse retourne un mot de passe non vide de 16 caractères")
    void reinitialiserMotDePasse_returnsNonEmptyPassword() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("membre@test.com");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        doNothing().when(adminUserServiceClient).resetPassword(anyString(), anyString(), any(PasswordResetRequestDTO.class));

        String password = syncService.reinitialiserMotDePasse(membreId);

        assertThat(password).isNotBlank().hasSize(16);
    }

    @Test
    @DisplayName("reinitialiserMotDePasse passe temporary=false dans la requête")
    void reinitialiserMotDePasse_setsTemporaryFalse() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("membre@test.com");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        doNothing().when(adminUserServiceClient).resetPassword(anyString(), anyString(), any(PasswordResetRequestDTO.class));

        syncService.reinitialiserMotDePasse(membreId);

        ArgumentCaptor<PasswordResetRequestDTO> captor = ArgumentCaptor.forClass(PasswordResetRequestDTO.class);
        verify(adminUserServiceClient).resetPassword(anyString(), anyString(), captor.capture());
        assertThat(captor.getValue().isTemporary()).isFalse();
        assertThat(captor.getValue().getPassword()).isNotBlank();
    }

    @Test
    @DisplayName("reinitialiserMotDePasse propage l'exception si adminUserServiceClient échoue")
    void reinitialiserMotDePasse_propagatesExceptionOnClientFailure() {
        UUID membreId = UUID.randomUUID();
        UUID keycloakId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setKeycloakId(keycloakId);
        membre.setEmail("membre@test.com");

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        doThrow(new RuntimeException("Keycloak unreachable"))
                .when(adminUserServiceClient).resetPassword(anyString(), anyString(), any(PasswordResetRequestDTO.class));

        assertThatThrownBy(() -> syncService.reinitialiserMotDePasse(membreId))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Keycloak unreachable");
    }
}
