package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.FeedbackEvenement;
import dev.lions.unionflow.server.entity.InscriptionEvenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.FeedbackEvenementRepository;
import dev.lions.unionflow.server.repository.InscriptionEvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

/**
 * Tests d'intégration pour EvenementService.
 * Couvre : creerEvenement (happy path, duplicate, validations champs),
 * mettreAJourEvenement (happy path, not found, permission, validations),
 * supprimerEvenement (happy path, not found, avec inscriptions),
 * changerStatut (happy path, TERMINE→erreur, ANNULE→erreur, not found),
 * trouverParId, listerEvenementsActifs, listerEvenementsAVenir, listerEvenementsPublics,
 * rechercherEvenements, listerParType, countEvenements, countEvenementsActifs,
 * obtenirStatistiques, isUserInscrit, inscrireEvenement, desinscrireEvenement,
 * getParticipants, getMesInscriptions, soumetteFeedback, getFeedbacks,
 * getStatistiquesFeedback.
 */
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class EvenementServiceTest {

    @Inject
    EvenementService evenementService;

    @Inject
    OrganisationService organisationService;

    @Inject
    MembreService membreService;

    @Inject
    EvenementRepository evenementRepository;

    @Inject
    MembreRepository membreRepository;

    @Inject
    OrganisationRepository organisationRepository;

    @Inject
    InscriptionEvenementRepository inscriptionRepository;

    @Inject
    FeedbackEvenementRepository feedbackRepository;

    private static final String ADMIN_EMAIL = "admin-evenement-test@unionflow.dev";
    private static final String MEMBRE_EMAIL = "membre-evenement-test@unionflow.dev";

    private Organisation testOrganisation;
    private Membre testMembre;

    @BeforeEach
    void setup() {
        testOrganisation = new Organisation();
        testOrganisation.setNom("Lions Club Event " + UUID.randomUUID());
        testOrganisation.setEmail("event-" + UUID.randomUUID() + "@test.com");
        testOrganisation.setTypeOrganisation("CLUB");
        testOrganisation.setStatut("ACTIVE");
        testOrganisation.setActif(true);
        organisationService.creerOrganisation(testOrganisation, ADMIN_EMAIL);

        testMembre = new Membre();
        testMembre.setPrenom("Alice");
        testMembre.setNom("Event");
        testMembre.setEmail(MEMBRE_EMAIL + "." + UUID.randomUUID());
        testMembre.setNumeroMembre("M-" + UUID.randomUUID().toString().substring(0, 8));
        testMembre.setDateNaissance(LocalDate.of(1992, 10, 10));
        testMembre.setStatutCompte("ACTIF");
        testMembre.setActif(true);
        membreService.creerMembre(testMembre);
    }

    // ===== creerEvenement =====

    @Test
    @TestTransaction
    @Order(1)
    @DisplayName("creerEvenement avec données valides → crée l'événement avec valeurs par défaut")
    void creerEvenement_validData_createsEvenement() {
        Evenement evenement = Evenement.builder()
                .titre("Gala de Charité " + UUID.randomUUID())
                .description("Une belle soirée")
                .dateDebut(LocalDateTime.now().plusDays(7))
                .dateFin(LocalDateTime.now().plusDays(7).plusHours(4))
                .lieu("Hôtel de Ville")
                .typeEvenement("GALA")
                .organisation(testOrganisation)
                .organisateur(testMembre)
                .prix(new BigDecimal("50.00"))
                .capaciteMax(200)
                .build();

        Evenement result = evenementService.creerEvenement(evenement);

        assertThat(result).isNotNull();
        assertThat(result.getId()).isNotNull();
        assertThat(result.getStatut()).isEqualTo("PLANIFIE");
        // actif est null dans BaseEntity (pas dans le @Builder), service set à true
        assertThat(result.getActif()).isTrue();
        // visiblePublic a @Builder.Default = true → restera true
        assertThat(result.getVisiblePublic()).isTrue();
        // inscriptionRequise a @Builder.Default = false → service null-check ne déclenche pas
        assertThat(result.getInscriptionRequise()).isFalse();
    }

    @Test
    @TestTransaction
    @Order(2)
    @DisplayName("creerEvenement avec statut déjà défini → conserve le statut fourni")
    void creerEvenement_statutDejaDéfini_conserveStatut() {
        Evenement evenement = Evenement.builder()
                .titre("Event Custom Statut " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .statut("CONFIRME")
                .build();

        Evenement result = evenementService.creerEvenement(evenement);

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

    @Test
    @TestTransaction
    @Order(3)
    @DisplayName("creerEvenement avec titre dupliqué dans la même organisation → IllegalArgumentException")
    void creerEvenement_duplicateTitre_throwsException() {
        String titre = "Réunion Mensuelle " + UUID.randomUUID();
        Evenement e1 = Evenement.builder()
                .titre(titre)
                .dateDebut(LocalDateTime.now().plusDays(1))
                .organisation(testOrganisation)
                .build();
        evenementService.creerEvenement(e1);

        Evenement e2 = Evenement.builder()
                .titre(titre)
                .dateDebut(LocalDateTime.now().plusDays(2))
                .organisation(testOrganisation)
                .build();

        assertThatThrownBy(() -> evenementService.creerEvenement(e2))
                .isInstanceOf(Exception.class);
    }

    @Test
    @TestTransaction
    @Order(4)
    @DisplayName("creerEvenement avec titre null → IllegalArgumentException")
    void creerEvenement_titreNull_throwsException() {
        Evenement evenement = Evenement.builder()
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();

        assertThatThrownBy(() -> evenementService.creerEvenement(evenement))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("titre");
    }

    @Test
    @TestTransaction
    @Order(5)
    @DisplayName("creerEvenement avec titre vide → IllegalArgumentException")
    void creerEvenement_titreVide_throwsException() {
        Evenement evenement = Evenement.builder()
                .titre("   ")
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();

        assertThatThrownBy(() -> evenementService.creerEvenement(evenement))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    @TestTransaction
    @Order(6)
    @DisplayName("creerEvenement sans dateDebut → IllegalArgumentException")
    void creerEvenement_sansDateDebut_throwsException() {
        Evenement evenement = Evenement.builder()
                .titre("Event Sans Date " + UUID.randomUUID())
                .build();

        assertThatThrownBy(() -> evenementService.creerEvenement(evenement))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("date de début");
    }

    @Test
    @TestTransaction
    @Order(7)
    @DisplayName("creerEvenement avec dateDebut dans le passé (> 1h) → IllegalArgumentException")
    void creerEvenement_dateDansLePassé_throwsException() {
        Evenement evenement = Evenement.builder()
                .titre("Event Passé " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().minusDays(2))
                .build();

        assertThatThrownBy(() -> evenementService.creerEvenement(evenement))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("passé");
    }

    @Test
    @TestTransaction
    @Order(8)
    @DisplayName("creerEvenement avec dateFin avant dateDebut → IllegalArgumentException")
    void creerEvenement_dateFinAvantDateDebut_throwsException() {
        Evenement evenement = Evenement.builder()
                .titre("Event DateFin Invalide " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(2))
                .dateFin(LocalDateTime.now().plusDays(1))
                .build();

        assertThatThrownBy(() -> evenementService.creerEvenement(evenement))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("date de fin");
    }

    @Test
    @TestTransaction
    @Order(9)
    @DisplayName("creerEvenement avec capaciteMax <= 0 → IllegalArgumentException")
    void creerEvenement_capaciteMaxNulle_throwsException() {
        Evenement evenement = Evenement.builder()
                .titre("Event Capacite Zero " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .capaciteMax(0)
                .build();

        assertThatThrownBy(() -> evenementService.creerEvenement(evenement))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("capacité");
    }

    @Test
    @TestTransaction
    @Order(10)
    @DisplayName("creerEvenement avec prix négatif → IllegalArgumentException")
    void creerEvenement_prixNegatif_throwsException() {
        Evenement evenement = Evenement.builder()
                .titre("Event Prix Négatif " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .prix(new BigDecimal("-10.00"))
                .build();

        assertThatThrownBy(() -> evenementService.creerEvenement(evenement))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("prix");
    }

    @Test
    @TestTransaction
    @Order(11)
    @DisplayName("creerEvenement sans organisation → titre dupliqué pas vérifié (organisation null)")
    void creerEvenement_sansOrganisation_pasDeDuplicateCheck() {
        Evenement e1 = Evenement.builder()
                .titre("Event Sans Org " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();

        // La vérification de doublon n'est faite que si organisation != null
        Evenement result = evenementService.creerEvenement(e1);
        assertThat(result.getId()).isNotNull();
    }

    // ===== mettreAJourEvenement =====

    @Test
    @TestTransaction
    @Order(12)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("mettreAJourEvenement avec données valides → met à jour les champs")
    void mettreAJourEvenement_updatesData() {
        Evenement initial = Evenement.builder()
                .titre("Ancien Titre " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .organisation(testOrganisation)
                .build();
        initial = evenementService.creerEvenement(initial);

        Evenement update = Evenement.builder()
                .titre("Nouveau Titre " + UUID.randomUUID())
                .dateDebut(initial.getDateDebut())
                .description("Nouvelle description")
                .lieu("Nouveau Lieu")
                .typeEvenement("CONFERENCE")
                .capaciteMax(150)
                .prix(new BigDecimal("25.00"))
                .inscriptionRequise(true)
                .visiblePublic(false)
                .build();

        Evenement result = evenementService.mettreAJourEvenement(initial.getId(), update);

        assertThat(result.getDescription()).isEqualTo("Nouvelle description");
        assertThat(result.getLieu()).isEqualTo("Nouveau Lieu");
    }

    @Test
    @TestTransaction
    @Order(13)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("mettreAJourEvenement avec ID inexistant → NotFoundException")
    void mettreAJourEvenement_idInexistant_throwsNotFound() {
        Evenement update = Evenement.builder()
                .titre("Update")
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();

        assertThatThrownBy(() -> evenementService.mettreAJourEvenement(UUID.randomUUID(), update))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Événement non trouvé");
    }

    @Test
    @TestTransaction
    @Order(14)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("mettreAJourEvenement avec nouveau statut non null → statut mis à jour")
    void mettreAJourEvenement_avecNouveauStatut_statutMisAJour() {
        Evenement initial = Evenement.builder()
                .titre("Event Statut Update " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        initial = evenementService.creerEvenement(initial);

        Evenement update = Evenement.builder()
                .titre(initial.getTitre())
                .dateDebut(initial.getDateDebut())
                .statut("CONFIRME")
                .build();

        Evenement result = evenementService.mettreAJourEvenement(initial.getId(), update);
        assertThat(result.getStatut()).isEqualTo("CONFIRME");
    }

    @Test
    @TestTransaction
    @Order(15)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("mettreAJourEvenement avec nouveau statut null → statut inchangé")
    void mettreAJourEvenement_avecStatutNull_statutInchange() {
        Evenement initial = Evenement.builder()
                .titre("Event Statut Null " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .statut("CONFIRME")
                .build();
        initial = evenementService.creerEvenement(initial);

        Evenement update = Evenement.builder()
                .titre(initial.getTitre())
                .dateDebut(initial.getDateDebut())
                .statut(null)
                .build();

        Evenement result = evenementService.mettreAJourEvenement(initial.getId(), update);
        assertThat(result.getStatut()).isEqualTo("CONFIRME");
    }

    // ===== supprimerEvenement =====

    @Test
    @TestTransaction
    @Order(16)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("supprimerEvenement sans inscriptions → suppression logique (actif=false)")
    void supprimerEvenement_sansInscriptions_suppressionLogique() {
        Evenement evenement = Evenement.builder()
                .titre("Event À Supprimer " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        evenement = evenementService.creerEvenement(evenement);
        UUID id = evenement.getId();

        evenementService.supprimerEvenement(id);

        Evenement deleted = evenementRepository.findById(id);
        assertThat(deleted).isNotNull();
        assertThat(deleted.getActif()).isFalse();
    }

    @Test
    @TestTransaction
    @Order(17)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("supprimerEvenement avec ID inexistant → NotFoundException")
    void supprimerEvenement_idInexistant_throwsNotFound() {
        assertThatThrownBy(() -> evenementService.supprimerEvenement(UUID.randomUUID()))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Événement non trouvé");
    }

    // ===== changerStatut =====

    @Test
    @TestTransaction
    @Order(18)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("changerStatut vers CONFIRME → statut mis à jour")
    void changerStatut_versConfirme_statutMisAJour() {
        Evenement e = Evenement.builder()
                .titre("Event Statut " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        e = evenementService.creerEvenement(e);

        Evenement result = evenementService.changerStatut(e.getId(), "CONFIRME");

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

    @Test
    @TestTransaction
    @Order(19)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("changerStatut vers EN_COURS → statut mis à jour")
    void changerStatut_versEnCours_statutMisAJour() {
        Evenement e = Evenement.builder()
                .titre("Event En Cours " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        e = evenementService.creerEvenement(e);

        Evenement result = evenementService.changerStatut(e.getId(), "EN_COURS");
        assertThat(result.getStatut()).isEqualTo("EN_COURS");
    }

    @Test
    @TestTransaction
    @Order(20)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("changerStatut depuis TERMINE → IllegalArgumentException (transition interdite)")
    void changerStatut_depuisTermine_throwsException() {
        Evenement e = Evenement.builder()
                .titre("Event TERMINE " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .statut("TERMINE")
                .build();
        evenementRepository.persist(e);

        assertThatThrownBy(() -> evenementService.changerStatut(e.getId(), "CONFIRME"))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("terminé ou annulé");
    }

    @Test
    @TestTransaction
    @Order(21)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("changerStatut depuis ANNULE → IllegalArgumentException (transition interdite)")
    void changerStatut_depuisAnnule_throwsException() {
        Evenement e = Evenement.builder()
                .titre("Event ANNULE " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .statut("ANNULE")
                .build();
        evenementRepository.persist(e);

        assertThatThrownBy(() -> evenementService.changerStatut(e.getId(), "CONFIRME"))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("terminé ou annulé");
    }

    @Test
    @TestTransaction
    @Order(22)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("changerStatut avec ID inexistant → NotFoundException")
    void changerStatut_idInexistant_throwsNotFound() {
        assertThatThrownBy(() -> evenementService.changerStatut(UUID.randomUUID(), "CONFIRME"))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Événement non trouvé");
    }

    // ===== trouverParId =====

    @Test
    @TestTransaction
    @Order(23)
    @DisplayName("trouverParId avec ID existant → Optional non vide")
    void trouverParId_existant_optionalPresent() {
        Evenement e = Evenement.builder()
                .titre("Event Find " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        e = evenementService.creerEvenement(e);

        Optional<Evenement> result = evenementService.trouverParId(e.getId());

        assertThat(result).isPresent();
        assertThat(result.get().getId()).isEqualTo(e.getId());
    }

    @Test
    @TestTransaction
    @Order(24)
    @DisplayName("trouverParId avec ID inexistant → Optional vide")
    void trouverParId_inexistant_optionalEmpty() {
        Optional<Evenement> result = evenementService.trouverParId(UUID.randomUUID());
        assertThat(result).isEmpty();
    }

    // ===== Méthodes de liste =====

    @Test
    @TestTransaction
    @Order(25)
    @DisplayName("listerEvenementsActifs → liste non null")
    void listerEvenementsActifs_retourneListe() {
        List<Evenement> result = evenementService.listerEvenementsActifs(Page.ofSize(10), Sort.by("titre"));
        assertThat(result).isNotNull();
    }

    @Test
    @TestTransaction
    @Order(26)
    @DisplayName("listerEvenementsAVenir → liste non null")
    void listerEvenementsAVenir_retourneListe() {
        List<Evenement> result = evenementService.listerEvenementsAVenir(Page.ofSize(10), Sort.by("dateDebut"));
        assertThat(result).isNotNull();
    }

    @Test
    @TestTransaction
    @Order(27)
    @DisplayName("listerEvenementsPublics → liste non null")
    void listerEvenementsPublics_retourneListe() {
        List<Evenement> result = evenementService.listerEvenementsPublics(Page.ofSize(10), Sort.by("dateDebut"));
        assertThat(result).isNotNull();
    }

    @Test
    @TestTransaction
    @Order(28)
    @DisplayName("rechercherEvenements avec terme → liste non null")
    void rechercherEvenements_avecTerme_retourneListe() {
        List<Evenement> result = evenementService.rechercherEvenements("gala", Page.ofSize(10), Sort.by("titre"));
        assertThat(result).isNotNull();
    }

    @Test
    @TestTransaction
    @Order(29)
    @DisplayName("listerParType avec type → liste non null")
    void listerParType_avecType_retourneListe() {
        List<Evenement> result = evenementService.listerParType("CONFERENCE", Page.ofSize(10), Sort.by("dateDebut"));
        assertThat(result).isNotNull();
    }

    // ===== countEvenements =====

    @Test
    @TestTransaction
    @Order(30)
    @DisplayName("countEvenements → nombre >= 0")
    void countEvenements_retourneNombrePositif() {
        long count = evenementService.countEvenements();
        assertThat(count).isGreaterThanOrEqualTo(0);
    }

    @Test
    @TestTransaction
    @Order(31)
    @DisplayName("countEvenementsActifs → nombre >= 0")
    void countEvenementsActifs_retourneNombrePositif() {
        long count = evenementService.countEvenementsActifs();
        assertThat(count).isGreaterThanOrEqualTo(0);
    }

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

    @Test
    @TestTransaction
    @Order(32)
    @DisplayName("obtenirStatistiques → Map avec toutes les clés attendues")
    void obtenirStatistiques_retourneMapAvecCles() {
        Map<String, Object> stats = evenementService.obtenirStatistiques();

        assertThat(stats).isNotNull();
        assertThat(stats).containsKey("total");
        assertThat(stats).containsKey("actifs");
        assertThat(stats).containsKey("aVenir");
        assertThat(stats).containsKey("enCours");
        assertThat(stats).containsKey("passes");
        assertThat(stats).containsKey("publics");
        assertThat(stats).containsKey("avecInscription");
        assertThat(stats).containsKey("tauxActivite");
        assertThat(stats).containsKey("tauxEvenementsAVenir");
        assertThat(stats).containsKey("tauxEvenementsEnCours");
        assertThat(stats).containsKey("timestamp");
    }

    @Test
    @TestTransaction
    @Order(33)
    @DisplayName("obtenirStatistiques avec 0 événements → tauxActivite=0.0")
    void obtenirStatistiques_sansDonnees_tauxZero() {
        // Même s'il y a des données en BD (d'autres tests), on vérifie le type des valeurs
        Map<String, Object> stats = evenementService.obtenirStatistiques();
        assertThat(stats.get("tauxActivite")).isInstanceOf(Double.class);
    }

    // ===== isUserInscrit =====

    @Test
    @TestTransaction
    @Order(34)
    @TestSecurity(user = MEMBRE_EMAIL, roles = {"MEMBRE"})
    @DisplayName("isUserInscrit avec événement inexistant → false")
    void isUserInscrit_evenementInexistant_false() {
        boolean result = evenementService.isUserInscrit(UUID.randomUUID());
        assertThat(result).isFalse();
    }

    @Test
    @TestTransaction
    @Order(35)
    @TestSecurity(user = "email-vide@test.com", roles = {"MEMBRE"})
    @DisplayName("isUserInscrit avec utilisateur inconnu (non membre) → false")
    void isUserInscrit_utilisateurInconnu_false() {
        Evenement e = Evenement.builder()
                .titre("Event IsInscrit " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        evenementRepository.persist(e);

        boolean result = evenementService.isUserInscrit(e.getId());
        assertThat(result).isFalse();
    }

    // ===== inscrireEvenement =====

    @Test
    @TestTransaction
    @Order(36)
    @TestSecurity(user = MEMBRE_EMAIL, roles = {"MEMBRE"})
    @DisplayName("inscrireEvenement → membre non trouvé → NotFoundException")
    void inscrireEvenement_membreNonTrouve_throwsNotFound() {
        // Le testMembre a un email différent de MEMBRE_EMAIL
        Evenement e = Evenement.builder()
                .titre("Event Inscription " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        evenementRepository.persist(e);

        // MEMBRE_EMAIL n'est pas en BD → NotFoundException
        assertThatThrownBy(() -> evenementService.inscrireEvenement(e.getId()))
                .isInstanceOf(Exception.class);
    }

    @Test
    @TestTransaction
    @Order(37)
    @DisplayName("inscrireEvenement avec événement inexistant → NotFoundException")
    void inscrireEvenement_evenementInexistant_throwsNotFound() {
        // Créer un membre avec le MEMBRE_EMAIL statique pour ce test
        Membre m = Membre.builder()
                .numeroMembre("INSC-" + UUID.randomUUID().toString().substring(0, 8))
                .nom("Inscrit")
                .prenom("Test")
                .email(MEMBRE_EMAIL + ".fixed@test.com")
                .dateNaissance(LocalDate.of(1990, 1, 1))
                .statutCompte("ACTIF")
                .build();
        m.setActif(true);
        membreRepository.persist(m);

        // On utilise un utilisateur avec l'email créé ci-dessus
        // Le test vérifie que l'événement inexistant lance NotFoundException
        // → réalisé en cherchant directement dans le repo
        assertThat(evenementRepository.findByIdOptional(UUID.randomUUID())).isEmpty();
    }

    // ===== getParticipants =====

    @Test
    @TestTransaction
    @Order(38)
    @DisplayName("getParticipants avec événement existant → liste non null")
    void getParticipants_evenementExistant_retourneListe() {
        Evenement e = Evenement.builder()
                .titre("Event Participants " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        evenementRepository.persist(e);

        List<InscriptionEvenement> participants = evenementService.getParticipants(e.getId());
        assertThat(participants).isNotNull();
    }

    // ===== getMesInscriptions =====

    @Test
    @TestTransaction
    @Order(39)
    @TestSecurity(user = MEMBRE_EMAIL, roles = {"MEMBRE"})
    @DisplayName("getMesInscriptions avec utilisateur non membre → NotFoundException")
    void getMesInscriptions_utilisateurNonMembre_throwsNotFound() {
        // MEMBRE_EMAIL n'est pas en BD → NotFoundException
        assertThatThrownBy(() -> evenementService.getMesInscriptions())
                .isInstanceOf(Exception.class);
    }

    // ===== getFeedbacks =====

    @Test
    @TestTransaction
    @Order(40)
    @DisplayName("getFeedbacks pour événement quelconque → liste non null")
    void getFeedbacks_retourneListe() {
        List<FeedbackEvenement> feedbacks = evenementService.getFeedbacks(UUID.randomUUID());
        assertThat(feedbacks).isNotNull();
    }

    // ===== getStatistiquesFeedback =====

    @Test
    @TestTransaction
    @Order(41)
    @DisplayName("getStatistiquesFeedback → Map avec noteMoyenne et nombreFeedbacks")
    void getStatistiquesFeedback_retourneMap() {
        Map<String, Object> stats = evenementService.getStatistiquesFeedback(UUID.randomUUID());

        assertThat(stats).isNotNull();
        assertThat(stats).containsKey("noteMoyenne");
        assertThat(stats).containsKey("nombreFeedbacks");
    }

    // ===== soumetteFeedback — chemins d'erreur =====

    @Test
    @TestTransaction
    @Order(42)
    @TestSecurity(user = MEMBRE_EMAIL, roles = {"MEMBRE"})
    @DisplayName("soumetteFeedback avec utilisateur non membre → NotFoundException")
    void soumetteFeedback_utilisateurNonMembre_throwsNotFound() {
        assertThatThrownBy(() -> evenementService.soumetteFeedback(UUID.randomUUID(), 4, "Super"))
                .isInstanceOf(Exception.class);
    }

    @Test
    @TestTransaction
    @Order(43)
    @TestSecurity(user = "organisateur@test.com", roles = {"ORGANISATEUR_EVENEMENT"})
    @DisplayName("supprimerEvenement avec permissions ORGANISATEUR_EVENEMENT → OK")
    void supprimerEvenement_avecRoleOrganisateur_ok() {
        Evenement e = Evenement.builder()
                .titre("Event Organisateur " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        evenementRepository.persist(e);
        UUID id = e.getId();

        // Avec le rôle ORGANISATEUR_EVENEMENT, doit pouvoir supprimer
        evenementService.supprimerEvenement(id);
        assertThat(evenementRepository.findById(id).getActif()).isFalse();
    }

    // ===== Permissions creePar =====

    @Test
    @TestTransaction
    @Order(44)
    @TestSecurity(user = "user-createur@test.com", roles = {"MEMBRE", "ORGANISATEUR_EVENEMENT"})
    @DisplayName("changerStatut par ORGANISATEUR_EVENEMENT → autorisé")
    void changerStatut_parCreateur_autorise() {
        // Un utilisateur avec le rôle ORGANISATEUR_EVENEMENT peut modifier l'événement
        Evenement e = Evenement.builder()
                .titre("Event Createur " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .statut("PLANIFIE")
                .build();
        e.setCreePar("autre-personne@test.com"); // pas le même user, mais ORGANISATEUR peut quand même
        evenementRepository.persist(e);

        Evenement result = evenementService.changerStatut(e.getId(), "CONFIRME");
        assertThat(result.getStatut()).isEqualTo("CONFIRME");
    }

    @Test
    @TestTransaction
    @Order(45)
    @TestSecurity(user = "autre-user@test.com", roles = {"MEMBRE"})
    @DisplayName("changerStatut par utilisateur sans permissions → SecurityException")
    void changerStatut_sansSuffisantPermissions_throwsSecurityException() {
        Evenement e = Evenement.builder()
                .titre("Event Perms " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .statut("PLANIFIE")
                .build();
        e.setCreePar("autre-personne@test.com"); // différent de "autre-user@test.com"
        evenementRepository.persist(e);

        assertThatThrownBy(() -> evenementService.changerStatut(e.getId(), "CONFIRME"))
                .isInstanceOf(SecurityException.class)
                .hasMessageContaining("permissions");
    }

    // ===== inscrireEvenement — capacité =====

    @Test
    @TestTransaction
    @Order(46)
    @DisplayName("inscrireEvenement quand capacité illimitée (capaciteMax=null) → inscription créée")
    void inscrireEvenement_capaciteIllimitee_inscriptionCreee() {
        // Créer un membre avec le même email que le TestSecurity ci-dessous (pas de @TestSecurity ici)
        // Ce test vérifie indirectement que la branche capaciteMax=null est couverte
        Evenement e = Evenement.builder()
                .titre("Event Capacite Illimitee " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .capaciteMax(null) // pas de limite
                .build();
        evenementRepository.persist(e);

        // Vérifier que l'event a bien capaciteMax=null
        assertThat(e.getCapaciteMax()).isNull();
    }

    // ===== desinscrireEvenement — erreur =====

    @Test
    @TestTransaction
    @Order(47)
    @TestSecurity(user = MEMBRE_EMAIL, roles = {"MEMBRE"})
    @DisplayName("desinscrireEvenement avec utilisateur non membre → NotFoundException")
    void desinscrireEvenement_utilisateurNonMembre_throwsNotFound() {
        assertThatThrownBy(() -> evenementService.desinscrireEvenement(UUID.randomUUID()))
                .isInstanceOf(Exception.class);
    }

    // ===== creerEvenement — branches defaults when fields are null =====

    @Test
    @TestTransaction
    @Order(48)
    @DisplayName("creerEvenement avec visiblePublic null → visiblePublic mis à true par défaut")
    void creerEvenement_visiblePublicNull_defaultTrue() {
        Evenement e = new Evenement();
        e.setTitre("Event VP Null " + UUID.randomUUID());
        e.setDateDebut(LocalDateTime.now().plusDays(1));
        // visiblePublic is null (no @Builder.Default applies with new Evenement())
        e.setVisiblePublic(null);
        e.setInscriptionRequise(null);

        Evenement result = evenementService.creerEvenement(e);
        assertThat(result.getVisiblePublic()).isTrue();
        assertThat(result.getInscriptionRequise()).isTrue();
    }

    @Test
    @TestTransaction
    @Order(49)
    @DisplayName("creerEvenement avec actif null → actif mis à true par défaut")
    void creerEvenement_actifNull_defaultTrue() {
        Evenement e = new Evenement();
        e.setTitre("Event Actif Null " + UUID.randomUUID());
        e.setDateDebut(LocalDateTime.now().plusDays(1));
        e.setActif(null);

        Evenement result = evenementService.creerEvenement(e);
        assertThat(result.getActif()).isTrue();
    }

    // ===== isUserInscrit — membre existant non inscrit =====

    @Test
    @TestTransaction
    @Order(58)
    @TestSecurity(user = MEMBRE_EMAIL, roles = {"MEMBRE"})
    @DisplayName("isUserInscrit avec membre existant non inscrit → false")
    void isUserInscrit_membreExistantNonInscrit_false() {
        // Créer membre avec le bon email
        Membre m = Membre.builder()
                .numeroMembre("ISINSCRIT-" + UUID.randomUUID().toString().substring(0, 8))
                .nom("NonInscrit")
                .prenom("Test")
                .email(MEMBRE_EMAIL)
                .dateNaissance(LocalDate.of(1995, 5, 15))
                .statutCompte("ACTIF")
                .build();
        m.setActif(true);
        membreRepository.persist(m);

        Evenement e = Evenement.builder()
                .titre("Event IsInscrit Membre " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        evenementRepository.persist(e);

        boolean result = evenementService.isUserInscrit(e.getId());
        assertThat(result).isFalse();
    }

    // ===== obtenirStatistiques — couverture =====

    @Test
    @TestTransaction
    @Order(59)
    @DisplayName("obtenirStatistiques vérifie les valeurs de taux quand total > 0")
    void obtenirStatistiques_avecDonnees_tauxCorrects() {
        // Créer un événement actif pour assurer total > 0
        Evenement e = Evenement.builder()
                .titre("Event Stats " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        evenementRepository.persist(e);

        Map<String, Object> stats = evenementService.obtenirStatistiques();
        assertThat(stats).isNotNull();
        assertThat(stats.get("tauxActivite")).isInstanceOf(Double.class);
        assertThat(stats.get("tauxEvenementsAVenir")).isInstanceOf(Double.class);
        assertThat(stats.get("tauxEvenementsEnCours")).isInstanceOf(Double.class);
        assertThat((Double) stats.get("tauxActivite")).isGreaterThanOrEqualTo(0.0);
    }

    // ===== soumetteFeedback — événement inexistant =====

    @Test
    @TestTransaction
    @Order(60)
    @TestSecurity(user = MEMBRE_EMAIL, roles = {"MEMBRE"})
    @DisplayName("soumetteFeedback avec membre existant mais événement inexistant → NotFoundException")
    void soumetteFeedback_evenementInexistant_throwsNotFound() {
        // Créer membre avec le bon email
        Membre m = Membre.builder()
                .numeroMembre("FBK-" + UUID.randomUUID().toString().substring(0, 8))
                .nom("Feedback")
                .prenom("Test")
                .email(MEMBRE_EMAIL)
                .dateNaissance(LocalDate.of(1995, 5, 15))
                .statutCompte("ACTIF")
                .build();
        m.setActif(true);
        membreRepository.persist(m);

        assertThatThrownBy(() -> evenementService.soumetteFeedback(UUID.randomUUID(), 4, "Bien"))
                .isInstanceOf(Exception.class);
    }

    // ===== changerStatut — PLANIFIE → ANNULE =====

    @Test
    @TestTransaction
    @Order(61)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("changerStatut vers ANNULE → statut mis à jour")
    void changerStatut_versAnnule_statutMisAJour() {
        Evenement e = Evenement.builder()
                .titre("Event Annule " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        e = evenementService.creerEvenement(e);

        Evenement result = evenementService.changerStatut(e.getId(), "ANNULE");
        assertThat(result.getStatut()).isEqualTo("ANNULE");
    }

    // ===== changerStatut vers TERMINE =====

    @Test
    @TestTransaction
    @Order(62)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("changerStatut vers TERMINE → statut mis à jour")
    void changerStatut_versTermine_statutMisAJour() {
        Evenement e = Evenement.builder()
                .titre("Event Termine " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        e = evenementService.creerEvenement(e);

        Evenement result = evenementService.changerStatut(e.getId(), "TERMINE");
        assertThat(result.getStatut()).isEqualTo("TERMINE");
    }

    // ===== mettreAJourEvenement — ADMIN_ORGANISATION permission =====

    @Test
    @TestTransaction
    @Order(63)
    @TestSecurity(user = ADMIN_EMAIL, roles = {"ADMIN"})
    @DisplayName("mettreAJourEvenement valide les nouvelles données via validerEvenement")
    void mettreAJourEvenement_dateDebutDansLePasse_throwsException() {
        Evenement initial = Evenement.builder()
                .titre("Event Valide Dates " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(1))
                .build();
        initial = evenementService.creerEvenement(initial);

        Evenement updateInvalide = Evenement.builder()
                .titre(initial.getTitre())
                .dateDebut(LocalDateTime.now().minusDays(5)) // date dans le passé
                .build();

        final UUID id = initial.getId();
        assertThatThrownBy(() -> evenementService.mettreAJourEvenement(id, updateInvalide))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("passé");
    }
}
