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.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;

import dev.lions.unionflow.server.api.dto.organisation.request.CreateOrganisationRequest;
import dev.lions.unionflow.server.api.dto.organisation.request.UpdateOrganisationRequest;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.TypeReference;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
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.Nested;
import org.junit.jupiter.api.Test;

@QuarkusTest
class OrganisationServiceTest {

    @Inject
    OrganisationService organisationService;

    @InjectMock
    OrganisationRepository organisationRepository;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    TypeReferenceRepository typeReferenceRepository;

    @InjectMock
    MembreOrganisationRepository membreOrganisationRepository;

    @InjectMock
    EvenementRepository evenementRepository;

    @InjectMock
    DefaultsService defaultsService;

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

    private Organisation orgFixture(String nom, String email) {
        Organisation o = new Organisation();
        o.setId(UUID.randomUUID());
        o.setNom(nom);
        o.setEmail(email);
        o.setTypeOrganisation("ASSOCIATION");
        o.setStatut("ACTIVE");
        o.setActif(true);
        o.setNombreMembres(0);
        o.setNombreAdministrateurs(0);
        o.setMembresOrganisations(new ArrayList<>());
        return o;
    }

    // =========================================================================
    // creerOrganisation
    // =========================================================================

    @Nested
    @DisplayName("creerOrganisation")
    class CreerOrganisationTests {

        @Test
        @DisplayName("Happy path: email et nom uniques, organisation créée")
        void creerOrganisation_success() {
            Organisation org = orgFixture("TestOrg", "testorg@test.dev");

            when(organisationRepository.findByEmail("testorg@test.dev")).thenReturn(Optional.empty());
            when(organisationRepository.findByNom("TestOrg")).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "user@test.dev");

            assertThat(created).isNotNull();
            assertThat(created.getCreePar()).isEqualTo("user@test.dev");
            assertThat(created.getModifiePar()).isEqualTo("user@test.dev");
            verify(organisationRepository).persist(org);
        }

        @Test
        @DisplayName("Utilisateur null: audit défini à 'system'")
        void creerOrganisation_nullUser_auditSystem() {
            Organisation org = orgFixture("SysOrg", "sysorg@test.dev");

            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, null);

            assertThat(created.getCreePar()).isEqualTo("system");
        }

        @Test
        @DisplayName("Utilisateur blank: audit défini à 'system'")
        void creerOrganisation_blankUser_auditSystem() {
            Organisation org = orgFixture("BlankOrg", "blank@test.dev");

            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "   ");

            assertThat(created.getCreePar()).isEqualTo("system");
        }

        @Test
        @DisplayName("Email déjà existant: lève IllegalStateException")
        void creerOrganisation_duplicateEmail_throws() {
            Organisation org = orgFixture("NewOrg", "dup@test.dev");
            when(organisationRepository.findByEmail("dup@test.dev"))
                    .thenReturn(Optional.of(orgFixture("Existing", "dup@test.dev")));

            assertThatThrownBy(() -> organisationService.creerOrganisation(org, "user@test.dev"))
                    .isInstanceOf(IllegalStateException.class)
                    .hasMessageContaining("email existe déjà");

            verify(organisationRepository, never()).persist(any(Organisation.class));
        }

        @Test
        @DisplayName("Nom déjà existant: lève IllegalArgumentException")
        void creerOrganisation_duplicateNom_throws() {
            Organisation org = orgFixture("DupNom", "unique@test.dev");
            when(organisationRepository.findByEmail("unique@test.dev")).thenReturn(Optional.empty());
            when(organisationRepository.findByNom("DupNom"))
                    .thenReturn(Optional.of(orgFixture("DupNom", "other@test.dev")));

            assertThatThrownBy(() -> organisationService.creerOrganisation(org, "user@test.dev"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("nom existe déjà");
        }

        @Test
        @DisplayName("Numéro d'enregistrement vide ('') : branche isEmpty() true → skip unicité")
        void creerOrganisation_emptyNumeroEnregistrement_skipsCheck() {
            Organisation org = orgFixture("OrgVide", "vide@test.dev");
            org.setNumeroEnregistrement(""); // non-null mais vide → !isEmpty() = false → skip
            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "user@test.dev");

            assertThat(created).isNotNull();
            verify(organisationRepository, never()).findByNumeroEnregistrement(anyString());
        }

        @Test
        @DisplayName("Numéro d'enregistrement déjà existant: lève IllegalArgumentException")
        void creerOrganisation_duplicateNumeroEnregistrement_throws() {
            Organisation org = orgFixture("UniqueOrg", "uniqueemail@test.dev");
            org.setNumeroEnregistrement("REG-001");
            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNumeroEnregistrement("REG-001"))
                    .thenReturn(Optional.of(orgFixture("OtherOrg", "other2@test.dev")));

            assertThatThrownBy(() -> organisationService.creerOrganisation(org, "user@test.dev"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("numéro d'enregistrement existe déjà");
        }

        @Test
        @DisplayName("Statut null: défini à 'ACTIVE'")
        void creerOrganisation_nullStatut_defaultsActive() {
            Organisation org = orgFixture("DefaultStatut", "defstatut@test.dev");
            org.setStatut(null);
            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "user@test.dev");

            assertThat(created.getStatut()).isEqualTo("ACTIVE");
        }

        @Test
        @DisplayName("TypeOrganisation null: défini à 'ASSOCIATION'")
        void creerOrganisation_nullType_defaultsAssociation() {
            Organisation org = orgFixture("DefaultType", "deftype@test.dev");
            org.setTypeOrganisation(null);
            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "user@test.dev");

            assertThat(created.getTypeOrganisation()).isEqualTo("ASSOCIATION");
        }

        @Test
        @DisplayName("DateCreation null: initialisée")
        void creerOrganisation_nullDateCreation_initialized() {
            Organisation org = orgFixture("NullDate", "nulldate@test.dev");
            org.setDateCreation(null);
            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "user@test.dev");

            assertThat(created.getDateCreation()).isNotNull();
        }

        @Test
        @DisplayName("DateCreation déjà définie: non écrasée (L108 false)")
        void creerOrganisation_dateCreationDejaDefinie_nonEcrasee() {
            Organisation org = orgFixture("WithDate", "withdate@test.dev");
            java.time.LocalDateTime existingDate = java.time.LocalDateTime.of(2025, 6, 1, 0, 0);
            org.setDateCreation(existingDate);
            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "user@test.dev");

            // dateCreation déjà définie → L108: if (null) = false → non écrasée
            assertThat(created.getDateCreation()).isEqualTo(existingDate);
        }

        @Test
        @DisplayName("NumeroEnregistrement non-null non-vide et unique → L89 isPresent=false → création réussie")
        void creerOrganisation_uniqueNumeroEnregistrement_success() {
            Organisation org = orgFixture("UniqueNumOrg", "uniquenum@test.dev");
            org.setNumeroEnregistrement("REG-UNIQUE-001");
            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            // isPresent() = false → branche L89 false → continue (no exception)
            when(organisationRepository.findByNumeroEnregistrement("REG-UNIQUE-001"))
                    .thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "user@test.dev");

            assertThat(created).isNotNull();
        }

        @Test
        @DisplayName("NumeroEnregistrement vide: pas de vérification unicité")
        void creerOrganisation_emptyNumeroEnregistrement_noCheck() {
            Organisation org = orgFixture("EmptyNum", "emptynum@test.dev");
            org.setNumeroEnregistrement("");
            when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(organisationRepository.findByNom(anyString())).thenReturn(Optional.empty());
            doNothing().when(organisationRepository).persist(any(Organisation.class));

            Organisation created = organisationService.creerOrganisation(org, "user@test.dev");

            assertThat(created).isNotNull();
            verify(organisationRepository, never()).findByNumeroEnregistrement(any());
        }
    }

    // =========================================================================
    // mettreAJourOrganisation
    // =========================================================================

    @Nested
    @DisplayName("mettreAJourOrganisation")
    class MettreAJourOrganisationTests {

        @Test
        @DisplayName("Happy path: même email et nom, champs mis à jour")
        void mettreAJourOrganisation_sameEmailAndNom() {
            UUID id = UUID.randomUUID();
            Organisation existing = orgFixture("Org1", "org1@test.dev");
            existing.setId(id);

            Organisation update = orgFixture("Org1", "org1@test.dev");
            update.setNombreMembres(5);
            update.setDevise("EUR");

            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
            when(defaultsService.getDevise()).thenReturn("EUR");

            Organisation result = organisationService.mettreAJourOrganisation(id, update, "user@test.dev");

            assertThat(result).isNotNull();
        }

        @Test
        @DisplayName("Email modifié, nouveau email disponible: mise à jour réussie")
        void mettreAJourOrganisation_newEmailAvailable() {
            UUID id = UUID.randomUUID();
            Organisation existing = orgFixture("Org2", "old@test.dev");
            existing.setId(id);

            Organisation update = orgFixture("Org2", "new@test.dev");
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
            when(organisationRepository.findByEmail("new@test.dev")).thenReturn(Optional.empty());
            when(defaultsService.getDevise()).thenReturn("XOF");

            Organisation result = organisationService.mettreAJourOrganisation(id, update, "user@test.dev");

            assertThat(result.getEmail()).isEqualTo("new@test.dev");
        }

        @Test
        @DisplayName("Email modifié mais déjà pris: lève IllegalStateException")
        void mettreAJourOrganisation_emailTaken_throws() {
            UUID id = UUID.randomUUID();
            Organisation existing = orgFixture("Org3", "old3@test.dev");
            existing.setId(id);

            Organisation update = orgFixture("Org3", "taken@test.dev");
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
            when(organisationRepository.findByEmail("taken@test.dev"))
                    .thenReturn(Optional.of(orgFixture("TakenOrg", "taken@test.dev")));

            assertThatThrownBy(() -> organisationService.mettreAJourOrganisation(id, update, "user@test.dev"))
                    .isInstanceOf(IllegalStateException.class)
                    .hasMessageContaining("email existe déjà");
        }

        @Test
        @DisplayName("Nom modifié, nouveau nom disponible: mise à jour réussie")
        void mettreAJourOrganisation_newNomAvailable() {
            UUID id = UUID.randomUUID();
            Organisation existing = orgFixture("OldName", "email@test.dev");
            existing.setId(id);

            Organisation update = orgFixture("NewName", "email@test.dev");
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
            when(organisationRepository.findByNom("NewName")).thenReturn(Optional.empty());
            when(defaultsService.getDevise()).thenReturn("XOF");

            Organisation result = organisationService.mettreAJourOrganisation(id, update, "user@test.dev");

            assertThat(result.getNom()).isEqualTo("NewName");
        }

        @Test
        @DisplayName("Nom modifié mais déjà pris: lève IllegalArgumentException")
        void mettreAJourOrganisation_nomTaken_throws() {
            UUID id = UUID.randomUUID();
            Organisation existing = orgFixture("OldName2", "email2@test.dev");
            existing.setId(id);

            Organisation update = orgFixture("TakenName", "email2@test.dev");
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
            when(organisationRepository.findByNom("TakenName"))
                    .thenReturn(Optional.of(orgFixture("TakenName", "other@test.dev")));

            assertThatThrownBy(() -> organisationService.mettreAJourOrganisation(id, update, "user@test.dev"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("nom existe déjà");
        }

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

            assertThatThrownBy(() -> organisationService.mettreAJourOrganisation(id, orgFixture("X", "x@test.dev"), "user@test.dev"))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("Statut null dans update: statut existant conservé")
        void mettreAJourOrganisation_nullStatut_keepsExisting() {
            UUID id = UUID.randomUUID();
            Organisation existing = orgFixture("OrgS", "orgs@test.dev");
            existing.setId(id);
            existing.setStatut("ACTIVE");

            Organisation update = orgFixture("OrgS", "orgs@test.dev");
            update.setStatut(null);
            update.setNombreMembres(0);
            update.setNombreAdministrateurs(0);
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
            when(defaultsService.getDevise()).thenReturn("XOF");

            Organisation result = organisationService.mettreAJourOrganisation(id, update, "user@test.dev");

            assertThat(result.getStatut()).isEqualTo("ACTIVE");
        }

        @Test
        @DisplayName("NiveauHierarchique null: défini à 0")
        void mettreAJourOrganisation_nullNiveauHierarchique_defaultsZero() {
            UUID id = UUID.randomUUID();
            Organisation existing = orgFixture("OrgH", "orgh@test.dev");
            existing.setId(id);

            Organisation update = orgFixture("OrgH", "orgh@test.dev");
            update.setNiveauHierarchique(null);
            update.setNombreMembres(0);
            update.setNombreAdministrateurs(0);
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
            when(defaultsService.getDevise()).thenReturn("XOF");

            Organisation result = organisationService.mettreAJourOrganisation(id, update, "user@test.dev");

            assertThat(result.getNiveauHierarchique()).isEqualTo(0);
        }
    }

    // =========================================================================
    // supprimerOrganisation
    // =========================================================================

    @Nested
    @DisplayName("supprimerOrganisation")
    class SupprimerOrganisationTests {

        @Test
        @DisplayName("Happy path: soft delete (actif=false, statut=DISSOUTE)")
        void supprimerOrganisation_success() {
            UUID id = UUID.randomUUID();
            Organisation org = orgFixture("ToDelete", "todelete@test.dev");
            org.setId(id);
            org.setNombreMembres(0);

            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(org));

            organisationService.supprimerOrganisation(id, "admin@test.dev");

            assertThat(org.getActif()).isFalse();
            assertThat(org.getStatut()).isEqualTo("DISSOUTE");
        }

        @Test
        @DisplayName("Organisation avec membres actifs: lève IllegalStateException")
        void supprimerOrganisation_withMembers_throws() {
            UUID id = UUID.randomUUID();
            Organisation org = orgFixture("HasMembers", "hasmembers@test.dev");
            org.setId(id);
            org.setNombreMembres(5);

            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(org));

            assertThatThrownBy(() -> organisationService.supprimerOrganisation(id, "admin@test.dev"))
                    .isInstanceOf(IllegalStateException.class)
                    .hasMessageContaining("membres actifs");
        }

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

            assertThatThrownBy(() -> organisationService.supprimerOrganisation(id, "admin@test.dev"))
                    .isInstanceOf(NotFoundException.class);
        }
    }

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

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

        @Test
        @DisplayName("trouverParId: retourne Optional avec organisation")
        void trouverParId_found() {
            UUID id = UUID.randomUUID();
            Organisation org = orgFixture("Found", "found@test.dev");
            org.setId(id);
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(org));

            Optional<Organisation> result = organisationService.trouverParId(id);

            assertThat(result).isPresent();
            assertThat(result.get().getNom()).isEqualTo("Found");
        }

        @Test
        @DisplayName("trouverParId: retourne empty si non trouvé")
        void trouverParId_notFound() {
            UUID id = UUID.randomUUID();
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.empty());

            Optional<Organisation> result = organisationService.trouverParId(id);

            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("trouverParEmail: retourne organisation si trouvée")
        void trouverParEmail_found() {
            Organisation org = orgFixture("ByEmail", "byemail@test.dev");
            when(organisationRepository.findByEmail("byemail@test.dev")).thenReturn(Optional.of(org));

            Optional<Organisation> result = organisationService.trouverParEmail("byemail@test.dev");

            assertThat(result).isPresent();
        }

        @Test
        @DisplayName("trouverParEmail: retourne empty si non trouvée")
        void trouverParEmail_notFound() {
            when(organisationRepository.findByEmail("nobody@test.dev")).thenReturn(Optional.empty());

            Optional<Organisation> result = organisationService.trouverParEmail("nobody@test.dev");

            assertThat(result).isEmpty();
        }
    }

    // =========================================================================
    // listerOrganisationsActives
    // =========================================================================

    @Nested
    @DisplayName("listerOrganisationsActives")
    class ListerOrganisationsActivesTests {

        @Test
        @DisplayName("Sans pagination: délègue au repository")
        void listerOrganisationsActives_delegates() {
            List<Organisation> orgs = List.of(orgFixture("Org1", "o1@test.dev"));
            when(organisationRepository.findAllActives()).thenReturn(orgs);

            List<Organisation> result = organisationService.listerOrganisationsActives();

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("Avec pagination: délègue au repository")
        void listerOrganisationsActives_paged() {
            List<Organisation> orgs = List.of(orgFixture("Org2", "o2@test.dev"));
            when(organisationRepository.findAllActives(any(), any())).thenReturn(orgs);

            List<Organisation> result = organisationService.listerOrganisationsActives(0, 10);

            assertThat(result).hasSize(1);
        }

        @Test
        @DisplayName("compterOrganisationsActives: délègue au repository")
        void compterOrganisationsActives() {
            when(organisationRepository.countActives()).thenReturn(7L);

            long count = organisationService.compterOrganisationsActives();

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

    // =========================================================================
    // rechercherOrganisations
    // =========================================================================

    @Test
    @DisplayName("rechercherOrganisations: délègue au repository")
    void rechercherOrganisations_delegates() {
        List<Organisation> orgs = List.of(orgFixture("Search", "search@test.dev"));
        when(organisationRepository.findByNomOrNomCourt(eq("test"), any(), any())).thenReturn(orgs);

        List<Organisation> result = organisationService.rechercherOrganisations("test", 0, 10);

        assertThat(result).hasSize(1);
    }

    // =========================================================================
    // rechercheAvancee
    // =========================================================================

    @Test
    @DisplayName("rechercheAvancee: délègue au repository")
    void rechercheAvancee_delegates() {
        List<Organisation> orgs = List.of(orgFixture("Avancee", "avancee@test.dev"));
        when(organisationRepository.rechercheAvancee(any(), any(), any(), any(), any(), any(), any()))
                .thenReturn(orgs);

        List<Organisation> result = organisationService.rechercheAvancee(
                "test", "ASSOCIATION", "ACTIVE", "Abidjan", "Lagunes", "CI", 0, 10);

        assertThat(result).hasSize(1);
    }

    // =========================================================================
    // activerOrganisation / suspendreOrganisation
    // =========================================================================

    @Nested
    @DisplayName("activerOrganisation / suspendreOrganisation")
    class ActivationTests {

        @Test
        @DisplayName("activerOrganisation: statut ACTIVE, actif=true")
        void activerOrganisation_success() {
            UUID id = UUID.randomUUID();
            Organisation org = orgFixture("Inactive", "inactive@test.dev");
            org.setId(id);
            org.setStatut("SUSPENDUE");
            org.setActif(false);

            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(org));

            Organisation result = organisationService.activerOrganisation(id, "admin@test.dev");

            assertThat(result.getStatut()).isEqualTo("ACTIVE");
            assertThat(result.getActif()).isTrue();
        }

        @Test
        @DisplayName("activerOrganisation: organisation introuvable lève NotFoundException")
        void activerOrganisation_notFound_throws() {
            UUID id = UUID.randomUUID();
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.empty());

            assertThatThrownBy(() -> organisationService.activerOrganisation(id, "admin@test.dev"))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("suspendreOrganisation: statut SUSPENDUE, accepteNouveauxMembres=false")
        void suspendreOrganisation_success() {
            UUID id = UUID.randomUUID();
            Organisation org = orgFixture("Active", "active@test.dev");
            org.setId(id);
            org.setStatut("ACTIVE");
            org.setAccepteNouveauxMembres(true);

            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(org));

            Organisation result = organisationService.suspendreOrganisation(id, "admin@test.dev");

            assertThat(result.getStatut()).isEqualTo("SUSPENDUE");
            assertThat(result.getAccepteNouveauxMembres()).isFalse();
        }

        @Test
        @DisplayName("suspendreOrganisation: organisation introuvable lève NotFoundException")
        void suspendreOrganisation_notFound_throws() {
            UUID id = UUID.randomUUID();
            when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.empty());

            assertThatThrownBy(() -> organisationService.suspendreOrganisation(id, "admin@test.dev"))
                    .isInstanceOf(NotFoundException.class);
        }
    }

    // =========================================================================
    // obtenirStatistiques
    // =========================================================================

    @Test
    @DisplayName("obtenirStatistiques: calcule et retourne la map complète")
    void obtenirStatistiques_returnsCompleteMap() {
        Organisation o1 = orgFixture("O1", "o1s@test.dev");
        o1.setTypeOrganisation("ASSOCIATION");
        Organisation o2 = orgFixture("O2", "o2s@test.dev");
        o2.setTypeOrganisation("COOPERATIVE");

        when(organisationRepository.count()).thenReturn(10L);
        when(organisationRepository.countActives()).thenReturn(7L);
        when(organisationRepository.countByStatut("SUSPENDUE")).thenReturn(2L);
        when(organisationRepository.countByStatut("DISSOLUE")).thenReturn(1L);
        when(organisationRepository.countNouvellesOrganisations(any())).thenReturn(3L);
        when(organisationRepository.listAll()).thenReturn(List.of(o1, o2));

        Map<String, Object> stats = organisationService.obtenirStatistiques();

        assertThat(stats).containsKey("totalAssociations");
        assertThat(stats).containsKey("associationsActives");
        assertThat(stats).containsKey("associationsInactives");
        assertThat(stats).containsKey("tauxActivite");
        assertThat(stats.get("totalAssociations")).isEqualTo(10L);
        assertThat(stats.get("associationsActives")).isEqualTo(7L);
        assertThat((Double) stats.get("tauxActivite")).isEqualTo(70.0);
        assertThat(stats).containsKey("repartitionParType");
    }

    @Test
    @DisplayName("obtenirStatistiques: taux d'activité 0 si pas d'organisations")
    void obtenirStatistiques_noOrgs_tauxZero() {
        when(organisationRepository.count()).thenReturn(0L);
        when(organisationRepository.countActives()).thenReturn(0L);
        when(organisationRepository.countByStatut(anyString())).thenReturn(0L);
        when(organisationRepository.countNouvellesOrganisations(any())).thenReturn(0L);
        when(organisationRepository.listAll()).thenReturn(List.of());

        Map<String, Object> stats = organisationService.obtenirStatistiques();

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

    @Test
    @DisplayName("obtenirStatistiques: organisation avec typeOrganisation null classifiée NON_DEFINI")
    void obtenirStatistiques_nullType_classifiedAsNonDefini() {
        Organisation o = orgFixture("O1", "o1n@test.dev");
        o.setTypeOrganisation(null);

        when(organisationRepository.count()).thenReturn(1L);
        when(organisationRepository.countActives()).thenReturn(1L);
        when(organisationRepository.countByStatut(anyString())).thenReturn(0L);
        when(organisationRepository.countNouvellesOrganisations(any())).thenReturn(0L);
        when(organisationRepository.listAll()).thenReturn(List.of(o));

        Map<String, Object> stats = organisationService.obtenirStatistiques();

        @SuppressWarnings("unchecked")
        Map<String, Long> repartition = (Map<String, Long>) stats.get("repartitionParType");
        assertThat(repartition).containsKey("NON_DEFINI");
    }

    // =========================================================================
    // listerOrganisationsPourUtilisateur
    // =========================================================================

    @Nested
    @DisplayName("listerOrganisationsPourUtilisateur")
    class ListerOrganisationsPourUtilisateurTests {

        @Test
        @DisplayName("Email null: retourne liste vide")
        void listerOrganisationsPourUtilisateur_nullEmail_returnsEmpty() {
            List<Organisation> result = organisationService.listerOrganisationsPourUtilisateur(null);
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("Email blank: retourne liste vide")
        void listerOrganisationsPourUtilisateur_blankEmail_returnsEmpty() {
            List<Organisation> result = organisationService.listerOrganisationsPourUtilisateur("   ");
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("Membre non trouvé: retourne liste vide")
        void listerOrganisationsPourUtilisateur_membreNotFound_returnsEmpty() {
            when(membreRepository.findByEmail("unknown@test.dev")).thenReturn(Optional.empty());

            List<Organisation> result = organisationService.listerOrganisationsPourUtilisateur("unknown@test.dev");

            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("Membre avec organisations: retourne la liste")
        void listerOrganisationsPourUtilisateur_withOrganisations() {
            Organisation org = orgFixture("UserOrg", "userorg@test.dev");

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

            Membre membre = new Membre();
            membre.setId(UUID.randomUUID());
            membre.setEmail("member@test.dev");
            membre.setMembresOrganisations(new ArrayList<>(List.of(mo)));

            when(membreRepository.findByEmail("member@test.dev")).thenReturn(Optional.of(membre));

            List<Organisation> result = organisationService.listerOrganisationsPourUtilisateur("member@test.dev");

            assertThat(result).hasSize(1);
            assertThat(result.get(0).getNom()).isEqualTo("UserOrg");
        }
    }

    // =========================================================================
    // associerUtilisateurAOrganisation
    // =========================================================================

    @Nested
    @DisplayName("associerUtilisateurAOrganisation")
    class AssocierUtilisateurTests {

        @Test
        @DisplayName("Email null: lève IllegalArgumentException")
        void associerUtilisateur_nullEmail_throws() {
            assertThatThrownBy(() -> organisationService.associerUtilisateurAOrganisation(null, UUID.randomUUID()))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("email est obligatoire");
        }

        @Test
        @DisplayName("Email blank: lève IllegalArgumentException")
        void associerUtilisateur_blankEmail_throws() {
            assertThatThrownBy(() -> organisationService.associerUtilisateurAOrganisation("  ", UUID.randomUUID()))
                    .isInstanceOf(IllegalArgumentException.class);
        }

        @Test
        @DisplayName("OrganisationId null: lève IllegalArgumentException")
        void associerUtilisateur_nullOrgId_throws() {
            assertThatThrownBy(() -> organisationService.associerUtilisateurAOrganisation("user@test.dev", null))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("organisation est obligatoire");
        }

        @Test
        @DisplayName("Organisation introuvable: lève NotFoundException")
        void associerUtilisateur_orgNotFound_throws() {
            UUID orgId = UUID.randomUUID();
            when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.empty());

            assertThatThrownBy(() -> organisationService.associerUtilisateurAOrganisation("user@test.dev", orgId))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("Utilisateur déjà associé: ne crée pas de nouveau MembreOrganisation")
        void associerUtilisateur_alreadyAssociated_noOp() {
            UUID orgId = UUID.randomUUID();
            Organisation org = orgFixture("Org", "org@test.dev");
            org.setId(orgId);

            Membre membre = new Membre();
            membre.setId(UUID.randomUUID());
            membre.setEmail("user@test.dev");
            membre.setMembresOrganisations(new ArrayList<>());

            when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
            when(membreRepository.findByEmail("user@test.dev")).thenReturn(Optional.of(membre));
            when(membreOrganisationRepository.findByMembreIdAndOrganisationId(membre.getId(), orgId))
                    .thenReturn(Optional.of(new MembreOrganisation()));

            organisationService.associerUtilisateurAOrganisation("user@test.dev", orgId);

            verify(membreOrganisationRepository, never()).persist(any(MembreOrganisation.class));
        }

        @Test
        @DisplayName("Nouveau utilisateur, email avec dot: membre minimal créé et associé")
        void associerUtilisateur_newUser_withDot_creates() {
            UUID orgId = UUID.randomUUID();
            Organisation org = orgFixture("Org", "org2@test.dev");
            org.setId(orgId);

            when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
            when(membreRepository.findByEmail("john.doe@test.dev")).thenReturn(Optional.empty());
            doNothing().when(membreRepository).persist(any(Membre.class));
            when(membreOrganisationRepository.findByMembreIdAndOrganisationId(any(), eq(orgId)))
                    .thenReturn(Optional.empty());
            doNothing().when(membreOrganisationRepository).persist(any(MembreOrganisation.class));

            organisationService.associerUtilisateurAOrganisation("john.doe@test.dev", orgId);

            verify(membreRepository).persist(any(Membre.class));
            verify(membreOrganisationRepository).persist(any(MembreOrganisation.class));
        }

        @Test
        @DisplayName("Nouveau utilisateur, email sans dot: membre minimal créé avec nom valide")
        void associerUtilisateur_newUser_withoutDot_creates() {
            UUID orgId = UUID.randomUUID();
            Organisation org = orgFixture("Org", "org3@test.dev");
            org.setId(orgId);

            when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
            when(membreRepository.findByEmail("adminuser@test.dev")).thenReturn(Optional.empty());
            doNothing().when(membreRepository).persist(any(Membre.class));
            when(membreOrganisationRepository.findByMembreIdAndOrganisationId(any(), eq(orgId)))
                    .thenReturn(Optional.empty());
            doNothing().when(membreOrganisationRepository).persist(any(MembreOrganisation.class));

            organisationService.associerUtilisateurAOrganisation("adminuser@test.dev", orgId);

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

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

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

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

        @Test
        @DisplayName("Organisation minimale sans typeOrganisation ni statut")
        void convertToResponse_minimal() {
            Organisation org = orgFixture("Min", "min@test.dev");
            org.setTypeOrganisation(null);
            org.setStatut(null);
            org.setOrganisationParente(null);
            org.setVersion(null);

            when(evenementRepository.countActifsByOrganisationId(org.getId())).thenReturn(0L);

            OrganisationResponse resp = organisationService.convertToResponse(org);

            assertThat(resp).isNotNull();
            assertThat(resp.getVersion()).isNull();
            assertThat(resp.getOrganisationParenteId()).isNull();
        }

        @Test
        @DisplayName("Organisation avec id null: nombre événements = 0")
        void convertToResponse_nullId_noEvenements() {
            Organisation org = new Organisation();
            org.setId(null);
            org.setNom("NullId");
            org.setEmail("nullid@test.dev");
            org.setNombreMembres(0);
            org.setNombreAdministrateurs(0);

            OrganisationResponse resp = organisationService.convertToResponse(org);

            assertThat(resp.getNombreEvenements()).isEqualTo(0);
            verify(evenementRepository, never()).countActifsByOrganisationId(any());
        }

        @Test
        @DisplayName("Organisation avec typeOrganisation: résout le libellé")
        void convertToResponse_withTypeOrganisation_resolvesLibelle() {
            Organisation org = orgFixture("TypeOrg", "typeorg@test.dev");
            org.setTypeOrganisation("ASSOCIATION");

            TypeReference ref = new TypeReference();
            ref.setLibelle("Association loi 1901");
            ref.setCouleur("info");

            when(evenementRepository.countActifsByOrganisationId(org.getId())).thenReturn(2L);
            when(typeReferenceRepository.findByDomaineAndCode("TYPE_ORGANISATION", "ASSOCIATION"))
                    .thenReturn(Optional.of(ref));
            when(typeReferenceRepository.findByDomaineAndCode("STATUT_ORGANISATION", "ACTIVE"))
                    .thenReturn(Optional.empty());

            OrganisationResponse resp = organisationService.convertToResponse(org);

            assertThat(resp.getTypeOrganisationLibelle()).isEqualTo("Association loi 1901");
            assertThat(resp.getNombreEvenements()).isEqualTo(2);
        }

        @Test
        @DisplayName("TypeOrganisation référence non trouvée: typeOrganisationLibelle = code")
        void convertToResponse_typeRefNotFound_usesCode() {
            Organisation org = orgFixture("UnknownType", "unknowntype@test.dev");
            org.setTypeOrganisation("UNKNOWN_TYPE");

            when(evenementRepository.countActifsByOrganisationId(org.getId())).thenReturn(0L);
            when(typeReferenceRepository.findByDomaineAndCode("TYPE_ORGANISATION", "UNKNOWN_TYPE"))
                    .thenReturn(Optional.empty());
            when(typeReferenceRepository.findByDomaineAndCode("STATUT_ORGANISATION", "ACTIVE"))
                    .thenReturn(Optional.empty());

            OrganisationResponse resp = organisationService.convertToResponse(org);

            assertThat(resp.getTypeOrganisationLibelle()).isEqualTo("UNKNOWN_TYPE");
        }

        @Test
        @DisplayName("Organisation avec statut: résout libellé et severity")
        void convertToResponse_withStatut_resolvesLibelle() {
            Organisation org = orgFixture("StatutOrg", "statutorg@test.dev");
            org.setStatut("ACTIVE");

            TypeReference statutRef = new TypeReference();
            statutRef.setLibelle("Active");
            statutRef.setCouleur("success");

            when(evenementRepository.countActifsByOrganisationId(org.getId())).thenReturn(0L);
            when(typeReferenceRepository.findByDomaineAndCode("TYPE_ORGANISATION", "ASSOCIATION"))
                    .thenReturn(Optional.empty());
            when(typeReferenceRepository.findByDomaineAndCode("STATUT_ORGANISATION", "ACTIVE"))
                    .thenReturn(Optional.of(statutRef));

            OrganisationResponse resp = organisationService.convertToResponse(org);

            assertThat(resp.getStatutLibelle()).isEqualTo("Active");
            assertThat(resp.getStatutSeverity()).isEqualTo("success");
        }

        @Test
        @DisplayName("Organisation avec parent: organisationParenteId et nom remplis")
        void convertToResponse_withParent() {
            UUID parentId = UUID.randomUUID();
            Organisation parent = orgFixture("Parent", "parent@test.dev");
            parent.setId(parentId);

            Organisation org = orgFixture("Child", "child@test.dev");
            org.setOrganisationParente(parent);

            when(evenementRepository.countActifsByOrganisationId(org.getId())).thenReturn(0L);
            when(typeReferenceRepository.findByDomaineAndCode(anyString(), anyString()))
                    .thenReturn(Optional.empty());

            OrganisationResponse resp = organisationService.convertToResponse(org);

            assertThat(resp.getOrganisationParenteId()).isEqualTo(parentId);
            assertThat(resp.getOrganisationParenteNom()).isEqualTo("Parent");
        }

        @Test
        @DisplayName("OrganisationPublique null: défini à true")
        void convertToResponse_nullOrganisationPublique_defaultsTrue() {
            Organisation org = orgFixture("PubNull", "pubnull@test.dev");
            org.setOrganisationPublique(null);

            when(evenementRepository.countActifsByOrganisationId(org.getId())).thenReturn(0L);
            when(typeReferenceRepository.findByDomaineAndCode(anyString(), anyString()))
                    .thenReturn(Optional.empty());

            OrganisationResponse resp = organisationService.convertToResponse(org);

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

        @Test
        @DisplayName("CotisationObligatoire null: défini à false")
        void convertToResponse_nullCotisationObligatoire_defaultsFalse() {
            Organisation org = orgFixture("CotNull", "cotnull@test.dev");
            org.setCotisationObligatoire(null);

            when(evenementRepository.countActifsByOrganisationId(org.getId())).thenReturn(0L);
            when(typeReferenceRepository.findByDomaineAndCode(anyString(), anyString()))
                    .thenReturn(Optional.empty());

            OrganisationResponse resp = organisationService.convertToResponse(org);

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

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

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

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

        @Test
        @DisplayName("Organisation sans typeOrganisation: typeOrganisationLibelle null")
        void convertToSummaryResponse_noType() {
            Organisation org = orgFixture("Sum", "sum@test.dev");
            org.setTypeOrganisation(null);
            org.setStatut(null);

            OrganisationSummaryResponse resp = organisationService.convertToSummaryResponse(org);

            assertThat(resp).isNotNull();
            assertThat(resp.getTypeOrganisationLibelle()).isNull();
        }

        @Test
        @DisplayName("Organisation avec typeOrganisation: résout le libellé")
        void convertToSummaryResponse_withType_resolvesLibelle() {
            Organisation org = orgFixture("SumType", "sumtype@test.dev");
            org.setTypeOrganisation("COOPERATIVE");
            org.setStatut(null);

            TypeReference ref = new TypeReference();
            ref.setLibelle("Coopérative");
            when(typeReferenceRepository.findByDomaineAndCode("TYPE_ORGANISATION", "COOPERATIVE"))
                    .thenReturn(Optional.of(ref));

            OrganisationSummaryResponse resp = organisationService.convertToSummaryResponse(org);

            assertThat(resp.getTypeOrganisationLibelle()).isEqualTo("Coopérative");
        }

        @Test
        @DisplayName("Référence type non trouvée: typeOrganisationLibelle = code")
        void convertToSummaryResponse_typeRefNotFound_usesCode() {
            Organisation org = orgFixture("SumFall", "sumfall@test.dev");
            org.setTypeOrganisation("UNKNOWN");
            org.setStatut(null);

            when(typeReferenceRepository.findByDomaineAndCode("TYPE_ORGANISATION", "UNKNOWN"))
                    .thenReturn(Optional.empty());

            OrganisationSummaryResponse resp = organisationService.convertToSummaryResponse(org);

            assertThat(resp.getTypeOrganisationLibelle()).isEqualTo("UNKNOWN");
        }

        @Test
        @DisplayName("Organisation avec statut, référence trouvée: libellé et severity remplis")
        void convertToSummaryResponse_withStatut_resolvesAll() {
            Organisation org = orgFixture("SumStatut", "sumstatut@test.dev");
            org.setTypeOrganisation(null);
            org.setStatut("SUSPENDUE");

            TypeReference ref = new TypeReference();
            ref.setLibelle("Suspendue");
            ref.setCouleur("warning");

            when(typeReferenceRepository.findByDomaineAndCode("STATUT_ORGANISATION", "SUSPENDUE"))
                    .thenReturn(Optional.of(ref));

            OrganisationSummaryResponse resp = organisationService.convertToSummaryResponse(org);

            assertThat(resp.getStatutLibelle()).isEqualTo("Suspendue");
            assertThat(resp.getStatutSeverity()).isEqualTo("warning");
        }

        @Test
        @DisplayName("Statut référence non trouvée: libellé = statut")
        void convertToSummaryResponse_statutRefNotFound_usesCode() {
            Organisation org = orgFixture("SumFallSt", "sumfallst@test.dev");
            org.setTypeOrganisation(null);
            org.setStatut("ACTIVE");

            when(typeReferenceRepository.findByDomaineAndCode("STATUT_ORGANISATION", "ACTIVE"))
                    .thenReturn(Optional.empty());

            OrganisationSummaryResponse resp = organisationService.convertToSummaryResponse(org);

            assertThat(resp.getStatutLibelle()).isEqualTo("ACTIVE");
            assertThat(resp.getStatutSeverity()).isNull();
        }
    }

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

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

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

        @Test
        @DisplayName("Request complet: tous les champs copiés")
        void convertFromCreateRequest_fullRequest() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            CreateOrganisationRequest req = CreateOrganisationRequest.builder()
                    .nom("Test Association")
                    .nomCourt("TA")
                    .description("Description")
                    .email("ta@test.dev")
                    .telephone("0700000001")
                    .telephoneSecondaire("0700000002")
                    .emailSecondaire("ta2@test.dev")
                    .siteWeb("https://ta.org")
                    .logo("https://ta.org/logo.png")
                    .typeOrganisation("ASSOCIATION")
                    .statut("ACTIVE")
                    .dateFondation(LocalDate.of(2010, 1, 15))
                    .numeroEnregistrement("REG-2010-001")
                    .devise("XOF")
                    .budgetAnnuel(BigDecimal.valueOf(1000000))
                    .cotisationObligatoire(true)
                    .montantCotisationAnnuelle(BigDecimal.valueOf(5000))
                    .objectifs("Promouvoir la culture")
                    .activitesPrincipales("Formation, sensibilisation")
                    .certifications("ISO 9001")
                    .partenaires("Partenaire A")
                    .notes("Notes diverses")
                    .build();

            Organisation org = organisationService.convertFromCreateRequest(req);

            assertThat(org).isNotNull();
            assertThat(org.getNom()).isEqualTo("Test Association");
            assertThat(org.getNomCourt()).isEqualTo("TA");
            assertThat(org.getEmail()).isEqualTo("ta@test.dev");
            assertThat(org.getTypeOrganisation()).isEqualTo("ASSOCIATION");
            assertThat(org.getStatut()).isEqualTo("ACTIVE");
            assertThat(org.getCotisationObligatoire()).isTrue();
        }

        @Test
        @DisplayName("typeOrganisation null: défaut ASSOCIATION")
        void convertFromCreateRequest_nullType_defaultsAssociation() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            CreateOrganisationRequest req = CreateOrganisationRequest.builder()
                    .nom("NoType")
                    .email("notype@test.dev")
                    .typeOrganisation(null)
                    .statut("ACTIVE")
                    .build();

            Organisation org = organisationService.convertFromCreateRequest(req);

            assertThat(org.getTypeOrganisation()).isEqualTo("ASSOCIATION");
        }

        @Test
        @DisplayName("statut null: défaut ACTIVE")
        void convertFromCreateRequest_nullStatut_defaultsActive() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            CreateOrganisationRequest req = CreateOrganisationRequest.builder()
                    .nom("NoStatut")
                    .email("nostatut@test.dev")
                    .statut(null)
                    .build();

            Organisation org = organisationService.convertFromCreateRequest(req);

            assertThat(org.getStatut()).isEqualTo("ACTIVE");
        }

        @Test
        @DisplayName("devise null: utilise devise par défaut du service")
        void convertFromCreateRequest_nullDevise_usesDefault() {
            when(defaultsService.getDevise()).thenReturn("EUR");

            CreateOrganisationRequest req = CreateOrganisationRequest.builder()
                    .nom("NoDevise")
                    .email("nodevise@test.dev")
                    .devise(null)
                    .build();

            Organisation org = organisationService.convertFromCreateRequest(req);

            assertThat(org.getDevise()).isEqualTo("EUR");
        }

        @Test
        @DisplayName("cotisationObligatoire null: défaut false")
        void convertFromCreateRequest_nullCotisation_defaultsFalse() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            CreateOrganisationRequest req = CreateOrganisationRequest.builder()
                    .nom("NoCot")
                    .email("nocot@test.dev")
                    .cotisationObligatoire(null)
                    .build();

            Organisation org = organisationService.convertFromCreateRequest(req);

            assertThat(org.getCotisationObligatoire()).isFalse();
        }

        @Test
        @DisplayName("organisationPublique et accepteNouveauxMembres non-null: valeur conservée (couvre branche non-null L665-666)")
        void convertFromCreateRequest_nonNullOrganisationPubliqueEtAccepte_keepsValue() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            CreateOrganisationRequest req = CreateOrganisationRequest.builder()
                    .nom("PublicOrg")
                    .email("public@test.dev")
                    .organisationPublique(false)
                    .accepteNouveauxMembres(false)
                    .build();

            Organisation org = organisationService.convertFromCreateRequest(req);

            assertThat(org.getOrganisationPublique()).isFalse();
            assertThat(org.getAccepteNouveauxMembres()).isFalse();
        }
    }

    // =========================================================================
    // convertFromUpdateRequest
    // =========================================================================

    @Nested
    @DisplayName("convertFromUpdateRequest")
    class ConvertFromUpdateRequestTests {

        @Test
        @DisplayName("null retourne null")
        void convertFromUpdateRequest_null() {
            assertThat(organisationService.convertFromUpdateRequest(null)).isNull();
        }

        @Test
        @DisplayName("Request complet: tous les champs copiés")
        void convertFromUpdateRequest_fullRequest() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            UpdateOrganisationRequest req = UpdateOrganisationRequest.builder()
                    .nom("Updated Org")
                    .nomCourt("UO")
                    .description("Updated description")
                    .email("updated@test.dev")
                    .telephone("0700000003")
                    .typeOrganisation("COOPERATIVE")
                    .statut("SUSPENDUE")
                    .devise("USD")
                    .cotisationObligatoire(false)
                    .build();

            Organisation org = organisationService.convertFromUpdateRequest(req);

            assertThat(org).isNotNull();
            assertThat(org.getNom()).isEqualTo("Updated Org");
            assertThat(org.getEmail()).isEqualTo("updated@test.dev");
            assertThat(org.getTypeOrganisation()).isEqualTo("COOPERATIVE");
            assertThat(org.getDevise()).isEqualTo("USD");
        }

        @Test
        @DisplayName("devise null: utilise devise par défaut du service")
        void convertFromUpdateRequest_nullDevise_usesDefault() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            UpdateOrganisationRequest req = UpdateOrganisationRequest.builder()
                    .nom("NoDeviseUpd")
                    .email("nodevisupd@test.dev")
                    .devise(null)
                    .build();

            Organisation org = organisationService.convertFromUpdateRequest(req);

            assertThat(org.getDevise()).isEqualTo("XOF");
        }

        @Test
        @DisplayName("cotisationObligatoire null: défaut false")
        void convertFromUpdateRequest_nullCotisation_defaultsFalse() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            UpdateOrganisationRequest req = UpdateOrganisationRequest.builder()
                    .nom("NoCotUpd")
                    .email("nocotupd@test.dev")
                    .cotisationObligatoire(null)
                    .build();

            Organisation org = organisationService.convertFromUpdateRequest(req);

            assertThat(org.getCotisationObligatoire()).isFalse();
        }

        @Test
        @DisplayName("organisationPublique et accepteNouveauxMembres non-null: valeur conservée (couvre branche non-null L707-708)")
        void convertFromUpdateRequest_nonNullOrganisationPubliqueEtAccepte_keepsValue() {
            when(defaultsService.getDevise()).thenReturn("XOF");

            UpdateOrganisationRequest req = UpdateOrganisationRequest.builder()
                    .nom("PublicOrgUpd")
                    .email("publicupd@test.dev")
                    .organisationPublique(false)
                    .accepteNouveauxMembres(false)
                    .build();

            Organisation org = organisationService.convertFromUpdateRequest(req);

            assertThat(org.getOrganisationPublique()).isFalse();
            assertThat(org.getAccepteNouveauxMembres()).isFalse();
        }
    }

    // =========================================================================
    // mettreAJourOrganisation – branches null pour nombreMembres/nombreAdministrateurs/
    // cotisationObligatoire/organisationPublique/accepteNouveauxMembres
    // =========================================================================

    @Test
    @DisplayName("mettreAJourOrganisation : tous les champs nullable null → valeurs par défaut appliquées")
    void mettreAJourOrganisation_allNullableFieldsNull_usesDefaults() {
        UUID id = UUID.randomUUID();
        Organisation existing = orgFixture("OrgDefaults", "orgdefaults@test.dev");
        existing.setId(id);

        // Update avec tous les champs nullable à null
        Organisation update = new Organisation();
        update.setNom("OrgDefaults");
        update.setEmail("orgdefaults@test.dev");
        update.setTypeOrganisation("ASSOCIATION");
        update.setNiveauHierarchique(null);
        update.setNombreMembres(null);
        update.setNombreAdministrateurs(null);
        update.setCotisationObligatoire(null);
        update.setOrganisationPublique(null);
        update.setAccepteNouveauxMembres(null);

        when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
        when(defaultsService.getDevise()).thenReturn("XOF");

        Organisation result = organisationService.mettreAJourOrganisation(id, update, "user@test.dev");

        assertThat(result.getNiveauHierarchique()).isEqualTo(0);
        assertThat(result.getNombreMembres()).isEqualTo(0);
        assertThat(result.getNombreAdministrateurs()).isEqualTo(0);
        assertThat(result.getCotisationObligatoire()).isFalse();
        assertThat(result.getOrganisationPublique()).isTrue();
        assertThat(result.getAccepteNouveauxMembres()).isTrue();
    }

    @Test
    @DisplayName("mettreAJourOrganisation : devise null → defaultsService.getDevise() (couvre L187)")
    void mettreAJourOrganisation_deviseNull_usesDefault() {
        UUID id = UUID.randomUUID();
        Organisation existing = orgFixture("OrgDevise", "orgdevise@test.dev");
        existing.setId(id);

        Organisation update = new Organisation();
        update.setNom("OrgDevise");
        update.setEmail("orgdevise@test.dev");
        update.setTypeOrganisation("ASSOCIATION");
        update.setDevise(null); // force la branche null de L187

        when(organisationRepository.findByIdOptional(id)).thenReturn(Optional.of(existing));
        when(defaultsService.getDevise()).thenReturn("EUR");

        Organisation result = organisationService.mettreAJourOrganisation(id, update, "user@test.dev");

        assertThat(result.getDevise()).isEqualTo("EUR");
    }

    // =========================================================================
    // rechercherOrganisationsCount – les deux branches (recherche null/vide et non-vide)
    // =========================================================================

    @Test
    @DisplayName("rechercherOrganisationsCount : recherche null → délègue à count()")
    void rechercherOrganisationsCount_nullRecherche_delegatesToCount() {
        when(organisationRepository.count()).thenReturn(42L);

        long result = organisationService.rechercherOrganisationsCount(null);

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

    @Test
    @DisplayName("rechercherOrganisationsCount : recherche vide → délègue à count()")
    void rechercherOrganisationsCount_blankRecherche_delegatesToCount() {
        when(organisationRepository.count()).thenReturn(15L);

        long result = organisationService.rechercherOrganisationsCount("   ");

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

    @Test
    @DisplayName("rechercherOrganisationsCount : recherche non-vide → exécute requête JPQL")
    @SuppressWarnings("unchecked")
    void rechercherOrganisationsCount_withRecherche_executesJpqlQuery() {
        EntityManager em = mock(EntityManager.class);
        TypedQuery<Long> query = mock(TypedQuery.class);

        when(organisationRepository.getEntityManager()).thenReturn(em);
        when(em.createQuery(anyString(), eq(Long.class))).thenReturn(query);
        when(query.setParameter(eq("p"), anyString())).thenReturn(query);
        when(query.getSingleResult()).thenReturn(7L);

        long result = organisationService.rechercherOrganisationsCount("test");

        assertThat(result).isEqualTo(7L);
    }
}
