package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

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.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.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
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.Test;

/**
 * Tests unitaires (mocks) pour EvenementService.
 *
 * <p>Cette classe utilise @InjectMock pour contrôler KeycloakService et les repositories,
 * permettant de tester les chemins qui nécessitent un utilisateur authentifié avec un email précis.
 * Elle complète EvenementServiceTest (tests d'intégration avec DB réelle).
 */
@QuarkusTest
class EvenementServiceMockTest {

    @Inject
    EvenementService evenementService;

    @InjectMock
    KeycloakService keycloakService;

    @InjectMock
    EvenementRepository evenementRepository;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    OrganisationRepository organisationRepository;

    @InjectMock
    InscriptionEvenementRepository inscriptionRepository;

    @InjectMock
    FeedbackEvenementRepository feedbackRepository;

    private static final String USER_EMAIL = "membre-mock@unionflow.dev";

    private Membre membreFixture() {
        Membre m = new Membre();
        m.setId(UUID.randomUUID());
        m.setEmail(USER_EMAIL);
        m.setNom("Mock");
        m.setPrenom("User");
        m.setNumeroMembre("MOCK-0001");
        m.setDateNaissance(LocalDate.of(1990, 6, 15));
        m.setStatutCompte("ACTIF");
        m.setActif(true);
        m.setMembresOrganisations(new ArrayList<>());
        return m;
    }

    private Evenement evenementFixture() {
        Evenement e = Evenement.builder()
                .titre("Event Mock " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(3))
                .statut("PLANIFIE")
                .build();
        e.setId(UUID.randomUUID());
        e.setActif(true);
        return e;
    }

    @BeforeEach
    void setupDefaultMocks() {
        // Par défaut : utilisateur authentifié avec email connu
        when(keycloakService.isAuthenticated()).thenReturn(true);
        when(keycloakService.getCurrentUserEmail()).thenReturn(USER_EMAIL);
        when(keycloakService.hasRole(anyString())).thenReturn(false);
        when(keycloakService.hasAnyRole(any(String[].class))).thenReturn(false);
    }

    // ===== isUserInscrit — email null/blank =====

    @Test
    @DisplayName("isUserInscrit — email null (non authentifié) → false")
    void isUserInscrit_emailNull_returnsFalse() {
        when(keycloakService.getCurrentUserEmail()).thenReturn(null);
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));

        boolean result = evenementService.isUserInscrit(eventId);

        assertThat(result).isFalse();
    }

    @Test
    @DisplayName("isUserInscrit — email blank → false")
    void isUserInscrit_emailBlank_returnsFalse() {
        when(keycloakService.getCurrentUserEmail()).thenReturn("   ");
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));

        boolean result = evenementService.isUserInscrit(eventId);

        assertThat(result).isFalse();
    }

    @Test
    @DisplayName("isUserInscrit — événement existant + membre trouvé mais non inscrit → false")
    void isUserInscrit_membreTrouveNonInscrit_returnsFalse() {
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        Membre m = membreFixture();

        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        // isMemberInscrit: Evenement.inscriptions is empty → false

        boolean result = evenementService.isUserInscrit(eventId);

        assertThat(result).isFalse();
    }

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

    @Test
    @DisplayName("inscrireEvenement — email null → IllegalStateException")
    void inscrireEvenement_emailNull_throwsIllegalState() {
        when(keycloakService.getCurrentUserEmail()).thenReturn(null);

        assertThatThrownBy(() -> evenementService.inscrireEvenement(UUID.randomUUID()))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("non authentifié");
    }

    @Test
    @DisplayName("inscrireEvenement — email blank → IllegalStateException")
    void inscrireEvenement_emailBlank_throwsIllegalState() {
        when(keycloakService.getCurrentUserEmail()).thenReturn("");

        assertThatThrownBy(() -> evenementService.inscrireEvenement(UUID.randomUUID()))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("non authentifié");
    }

    @Test
    @DisplayName("inscrireEvenement — membre non trouvé → NotFoundException")
    void inscrireEvenement_membreNonTrouve_throwsNotFound() {
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> evenementService.inscrireEvenement(UUID.randomUUID()))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    @Test
    @DisplayName("inscrireEvenement — événement non trouvé → NotFoundException")
    void inscrireEvenement_evenementNonTrouve_throwsNotFound() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> evenementService.inscrireEvenement(eventId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Événement non trouvé");
    }

    @Test
    @DisplayName("inscrireEvenement — déjà inscrit → IllegalStateException")
    void inscrireEvenement_dejaInscrit_throwsIllegalState() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);

        InscriptionEvenement existante = new InscriptionEvenement();
        existante.setId(UUID.randomUUID());

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(inscriptionRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.of(existante));

        assertThatThrownBy(() -> evenementService.inscrireEvenement(eventId))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("déjà inscrit");
    }

    @Test
    @DisplayName("inscrireEvenement — capacité atteinte → IllegalStateException")
    void inscrireEvenement_capaciteAtteinte_throwsIllegalState() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        e.setCapaciteMax(10);

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(inscriptionRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.empty());
        when(inscriptionRepository.countConfirmeesByEvenement(eventId)).thenReturn(10L);

        assertThatThrownBy(() -> evenementService.inscrireEvenement(eventId))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("complet");
    }

    @Test
    @DisplayName("inscrireEvenement — capacité null (illimitée) + happy path → inscription créée")
    void inscrireEvenement_capaciteIllimitee_happyPath() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        e.setCapaciteMax(null); // illimitée

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(inscriptionRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.empty());
        doNothing().when(inscriptionRepository).persist(any(InscriptionEvenement.class));

        InscriptionEvenement result = evenementService.inscrireEvenement(eventId);

        assertThat(result).isNotNull();
        assertThat(result.getMembre()).isEqualTo(m);
        assertThat(result.getEvenement()).isEqualTo(e);
        assertThat(result.getStatut()).isEqualTo(InscriptionEvenement.StatutInscription.CONFIRMEE.name());
    }

    @Test
    @DisplayName("inscrireEvenement — capacité disponible → inscription créée")
    void inscrireEvenement_capaciteDisponible_happyPath() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        e.setCapaciteMax(50);

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(inscriptionRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.empty());
        when(inscriptionRepository.countConfirmeesByEvenement(eventId)).thenReturn(20L);
        doNothing().when(inscriptionRepository).persist(any(InscriptionEvenement.class));

        InscriptionEvenement result = evenementService.inscrireEvenement(eventId);

        assertThat(result).isNotNull();
        assertThat(result.getEvenement()).isEqualTo(e);
    }

    // ===== desinscrireEvenement =====

    @Test
    @DisplayName("desinscrireEvenement — email null → IllegalStateException")
    void desinscrireEvenement_emailNull_throwsIllegalState() {
        when(keycloakService.getCurrentUserEmail()).thenReturn(null);

        assertThatThrownBy(() -> evenementService.desinscrireEvenement(UUID.randomUUID()))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("non authentifié");
    }

    @Test
    @DisplayName("desinscrireEvenement — email blank → IllegalStateException")
    void desinscrireEvenement_emailBlank_throwsIllegalState() {
        when(keycloakService.getCurrentUserEmail()).thenReturn("  ");

        assertThatThrownBy(() -> evenementService.desinscrireEvenement(UUID.randomUUID()))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("non authentifié");
    }

    @Test
    @DisplayName("desinscrireEvenement — membre non trouvé → NotFoundException")
    void desinscrireEvenement_membreNonTrouve_throwsNotFound() {
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> evenementService.desinscrireEvenement(UUID.randomUUID()))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    @Test
    @DisplayName("desinscrireEvenement — inscription non trouvée → NotFoundException")
    void desinscrireEvenement_inscriptionNonTrouvee_throwsNotFound() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(inscriptionRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.empty());

        assertThatThrownBy(() -> evenementService.desinscrireEvenement(eventId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Inscription non trouvée");
    }

    @Test
    @DisplayName("desinscrireEvenement — happy path → softDelete appelé sur l'inscription")
    void desinscrireEvenement_happyPath_softDelete() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        InscriptionEvenement inscription = new InscriptionEvenement();
        inscription.setId(UUID.randomUUID());
        inscription.setActif(true);

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(inscriptionRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.of(inscription));
        // softDelete() est une méthode void sur le mock → doNothing par défaut
        // On simule le comportement réel via doAnswer pour pouvoir vérifier l'effet
        doAnswer(invocation -> {
            InscriptionEvenement insc = invocation.getArgument(0);
            insc.setActif(false);
            return null;
        }).when(inscriptionRepository).softDelete(any(InscriptionEvenement.class));

        evenementService.desinscrireEvenement(eventId);

        // Vérifier que softDelete a bien été appelé et a modifié l'inscription
        verify(inscriptionRepository).softDelete(inscription);
        assertThat(inscription.getActif()).isFalse();
    }

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

    @Test
    @DisplayName("getMesInscriptions — email null → IllegalStateException")
    void getMesInscriptions_emailNull_throwsIllegalState() {
        when(keycloakService.getCurrentUserEmail()).thenReturn(null);

        assertThatThrownBy(() -> evenementService.getMesInscriptions())
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("non authentifié");
    }

    @Test
    @DisplayName("getMesInscriptions — email blank → IllegalStateException")
    void getMesInscriptions_emailBlank_throwsIllegalState() {
        when(keycloakService.getCurrentUserEmail()).thenReturn("");

        assertThatThrownBy(() -> evenementService.getMesInscriptions())
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("non authentifié");
    }

    @Test
    @DisplayName("getMesInscriptions — membre non trouvé → NotFoundException")
    void getMesInscriptions_membreNonTrouve_throwsNotFound() {
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> evenementService.getMesInscriptions())
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    @Test
    @DisplayName("getMesInscriptions — membre trouvé → liste des inscriptions retournée")
    void getMesInscriptions_membreTrouve_retourneListe() {
        Membre m = membreFixture();

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(inscriptionRepository.findByMembre(m.getId())).thenReturn(new ArrayList<>());

        List<InscriptionEvenement> result = evenementService.getMesInscriptions();

        assertThat(result).isNotNull();
    }

    // ===== soumetteFeedback =====

    @Test
    @DisplayName("soumetteFeedback — email null → IllegalStateException")
    void soumetteFeedback_emailNull_throwsIllegalState() {
        when(keycloakService.getCurrentUserEmail()).thenReturn(null);

        assertThatThrownBy(() -> evenementService.soumetteFeedback(UUID.randomUUID(), 4, "Bien"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("non authentifié");
    }

    @Test
    @DisplayName("soumetteFeedback — email blank → IllegalStateException")
    void soumetteFeedback_emailBlank_throwsIllegalState() {
        when(keycloakService.getCurrentUserEmail()).thenReturn("  ");

        assertThatThrownBy(() -> evenementService.soumetteFeedback(UUID.randomUUID(), 5, "Super"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("non authentifié");
    }

    @Test
    @DisplayName("soumetteFeedback — membre non trouvé → NotFoundException")
    void soumetteFeedback_membreNonTrouve_throwsNotFound() {
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> evenementService.soumetteFeedback(UUID.randomUUID(), 3, "Moyen"))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Membre non trouvé");
    }

    @Test
    @DisplayName("soumetteFeedback — événement non trouvé → NotFoundException")
    void soumetteFeedback_evenementNonTrouve_throwsNotFound() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> evenementService.soumetteFeedback(eventId, 4, "Bien"))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Événement non trouvé");
    }

    @Test
    @DisplayName("soumetteFeedback — feedback déjà soumis → IllegalStateException")
    void soumetteFeedback_feedbackDejaExistant_throwsIllegalState() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        FeedbackEvenement existant = new FeedbackEvenement();

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(feedbackRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.of(existant));

        assertThatThrownBy(() -> evenementService.soumetteFeedback(eventId, 5, "Excellent"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("déjà soumis un feedback");
    }

    @Test
    @DisplayName("soumetteFeedback — membre non inscrit → IllegalStateException")
    void soumetteFeedback_membreNonInscrit_throwsIllegalState() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(feedbackRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.empty());
        when(inscriptionRepository.isMembreInscrit(m.getId(), eventId)).thenReturn(false);

        assertThatThrownBy(() -> evenementService.soumetteFeedback(eventId, 4, "Bien"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("Seuls les participants");
    }

    @Test
    @DisplayName("soumetteFeedback — événement non terminé → IllegalStateException")
    void soumetteFeedback_evenementNonTermine_throwsIllegalState() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        // dateFin dans le futur → événement non terminé
        e.setDateFin(LocalDateTime.now().plusDays(1));

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(feedbackRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.empty());
        when(inscriptionRepository.isMembreInscrit(m.getId(), eventId)).thenReturn(true);

        assertThatThrownBy(() -> evenementService.soumetteFeedback(eventId, 3, "Pas encore"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("après la fin de l'événement");
    }

    @Test
    @DisplayName("soumetteFeedback — dateFin null → IllegalStateException (non terminé)")
    void soumetteFeedback_datefinNull_throwsIllegalState() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        e.setDateFin(null); // pas de date de fin → non terminé

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(feedbackRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.empty());
        when(inscriptionRepository.isMembreInscrit(m.getId(), eventId)).thenReturn(true);

        assertThatThrownBy(() -> evenementService.soumetteFeedback(eventId, 4, "Test"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("après la fin de l'événement");
    }

    @Test
    @DisplayName("soumetteFeedback — happy path (event terminé, membre inscrit) → feedback créé")
    void soumetteFeedback_happyPath_feedbackCree() {
        Membre m = membreFixture();
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        // dateFin dans le passé → événement terminé
        e.setDateFin(LocalDateTime.now().minusDays(1));

        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(m));
        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(feedbackRepository.findByMembreAndEvenement(m.getId(), eventId))
                .thenReturn(Optional.empty());
        when(inscriptionRepository.isMembreInscrit(m.getId(), eventId)).thenReturn(true);
        doNothing().when(feedbackRepository).persist(any(FeedbackEvenement.class));

        FeedbackEvenement result = evenementService.soumetteFeedback(eventId, 5, "Excellent !");

        assertThat(result).isNotNull();
        assertThat(result.getNote()).isEqualTo(5);
        assertThat(result.getCommentaire()).isEqualTo("Excellent !");
        assertThat(result.getMembre()).isEqualTo(m);
        assertThat(result.getEvenement()).isEqualTo(e);
    }

    // ===== peutModifierEvenement — branche creePar match =====

    @Test
    @DisplayName("mettreAJourEvenement — utilisateur est le créateur (sans rôle ADMIN) → autorisé")
    void mettreAJourEvenement_createur_autorise() {
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        e.setCreePar(USER_EMAIL); // le créateur = utilisateur courant
        e.setDateDebut(LocalDateTime.now().plusDays(5));

        Evenement update = Evenement.builder()
                .titre("Nouveau Titre " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(5))
                .statut("CONFIRME")
                .build();

        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        // hasRole retourne false → peutModifier via creePar match
        when(keycloakService.hasRole("ADMIN")).thenReturn(false);
        when(keycloakService.hasRole("ORGANISATEUR_EVENEMENT")).thenReturn(false);
        when(evenementRepository.update(any(Evenement.class))).thenReturn(e);

        Evenement result = evenementService.mettreAJourEvenement(eventId, update);

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

    @Test
    @DisplayName("mettreAJourEvenement — utilisateur n'est pas créateur et pas ADMIN → SecurityException")
    void mettreAJourEvenement_nonCreateur_nonAdmin_throwsSecurityException() {
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        e.setCreePar("autre-utilisateur@test.com");
        e.setDateDebut(LocalDateTime.now().plusDays(5));

        Evenement update = Evenement.builder()
                .titre("Modification " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(5))
                .build();

        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(keycloakService.hasRole("ADMIN")).thenReturn(false);
        when(keycloakService.hasRole("ORGANISATEUR_EVENEMENT")).thenReturn(false);

        assertThatThrownBy(() -> evenementService.mettreAJourEvenement(eventId, update))
                .isInstanceOf(SecurityException.class)
                .hasMessageContaining("permissions");
    }

    @Test
    @DisplayName("supprimerEvenement — utilisateur est créateur (sans rôle ADMIN) → suppression autorisée")
    void supprimerEvenement_createur_autorise() {
        UUID eventId = UUID.randomUUID();
        // Utiliser builder pour que inscriptions soit initialisée (ArrayList vide → getNombreInscrits() == 0)
        Evenement e = Evenement.builder()
                .titre("Event Suppr " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(3))
                .statut("PLANIFIE")
                .build();
        e.setId(eventId);
        e.setActif(true);
        e.setCreePar(USER_EMAIL);

        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(keycloakService.hasRole("ADMIN")).thenReturn(false);
        when(keycloakService.hasRole("ORGANISATEUR_EVENEMENT")).thenReturn(false);
        when(evenementRepository.update(any(Evenement.class))).thenReturn(e);

        // Ne doit pas lancer d'exception
        evenementService.supprimerEvenement(eventId);

        assertThat(e.getActif()).isFalse();
    }

    @Test
    @DisplayName("supprimerEvenement — événement avec inscrits > 0 → IllegalStateException")
    void supprimerEvenement_avecInscrits_throwsIllegalState() {
        UUID eventId = UUID.randomUUID();
        // Utiliser builder pour avoir une liste inscriptions initialisée
        InscriptionEvenement insc = InscriptionEvenement.builder()
                .statut(InscriptionEvenement.StatutInscription.CONFIRMEE.name())
                .build();
        insc.setId(UUID.randomUUID());
        insc.setActif(true);

        List<InscriptionEvenement> inscriptions = new ArrayList<>();
        inscriptions.add(insc);

        Evenement e = Evenement.builder()
                .titre("Event Avec Inscrits " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(3))
                .statut("PLANIFIE")
                .inscriptions(inscriptions)
                .build();
        e.setId(eventId);
        e.setActif(true);
        e.setCreePar(USER_EMAIL);

        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(keycloakService.hasRole("ADMIN")).thenReturn(true);

        assertThatThrownBy(() -> evenementService.supprimerEvenement(eventId))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("inscriptions");
    }

    // ===== creerEvenement — branches statut null / non-null =====

    @Test
    @DisplayName("creerEvenement — statut null → setStatut('PLANIFIE') exécuté (branche true L87-88)")
    void creerEvenement_statutNull_setsPlanifie() {
        // Couvre la branche true de : if (evenement.getStatut() == null) → setStatut("PLANIFIE")
        Evenement e = Evenement.builder()
                .titre("Creer Event Statut Null " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(5))
                .statut(null)  // null → branche true → setStatut("PLANIFIE")
                .build();
        // organisation null → skip unicité check
        doNothing().when(evenementRepository).persist(any(Evenement.class));

        Evenement result = evenementService.creerEvenement(e);

        assertThat(result).isNotNull();
        assertThat(result.getStatut()).isEqualTo("PLANIFIE");
    }

    @Test
    @DisplayName("creerEvenement — statut non-null → statut conservé (branche false L87)")
    void creerEvenement_statutNonNull_statutConserve() {
        // Couvre la branche false de : if (evenement.getStatut() == null) → statut déjà défini → conservé
        Evenement e = Evenement.builder()
                .titre("Creer Event Statut NonNull " + UUID.randomUUID())
                .dateDebut(LocalDateTime.now().plusDays(5))
                .statut("CONFIRME")  // non-null → branche false → statut conservé
                .build();
        // organisation null → skip unicité check
        doNothing().when(evenementRepository).persist(any(Evenement.class));

        Evenement result = evenementService.creerEvenement(e);

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

    @Test
    @DisplayName("changerStatut — utilisateur est créateur (sans rôle ADMIN) → statut changé")
    void changerStatut_createur_autorise() {
        UUID eventId = UUID.randomUUID();
        Evenement e = evenementFixture();
        e.setId(eventId);
        e.setCreePar(USER_EMAIL);
        e.setStatut("PLANIFIE");

        when(evenementRepository.findByIdOptional(eventId)).thenReturn(Optional.of(e));
        when(keycloakService.hasRole("ADMIN")).thenReturn(false);
        when(keycloakService.hasRole("ORGANISATEUR_EVENEMENT")).thenReturn(false);
        when(evenementRepository.update(any(Evenement.class))).thenReturn(e);

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

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