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.Mockito.mock;
import static org.mockito.Mockito.when;

import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.MembreRoleRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.inject.Inject;

import java.security.Principal;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * Tests complémentaires pour {@link MembreService} — propagation d'exception dans
 * {@code searchMembresAdvanced} et {@code buildOrderByClause} avec liste de colonnes vide.
 */
@QuarkusTest
@DisplayName("MembreService — cas limites searchMembresAdvanced et buildOrderByClause")
class MembreServiceBranchesMissingTest {

    @Inject
    MembreService membreService;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    MembreRoleRepository membreRoleRepository;

    @InjectMock
    TypeReferenceRepository typeReferenceRepository;

    @InjectMock
    MembreImportExportService membreImportExportService;

    @InjectMock
    OrganisationService organisationService;

    @InjectMock
    SecurityIdentity securityIdentity;

    @BeforeEach
    void setupAnonymousSecurity() {
        // Par défaut : utilisateur sans rôle ADMIN_ORGANISATION
        Principal principal = () -> "user.test@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of());
    }

    // =========================================================================
    // L554-556 : catch(Exception e) dans searchMembresAdvanced
    //
    // Code source (L493-557) :
    //   try {
    //     ...
    //     long totalElements = countQueryTyped.getSingleResult();
    //     ...
    //   } catch (Exception e) {                                       ← L554
    //     LOG.errorf(e, "Erreur lors de la recherche avancée de membres");  ← L555
    //     throw new RuntimeException("Erreur lors de la recherche avancée", e);  ← L556
    //   }
    //
    // La branche catch est dans le try block interne qui utilise @PersistenceContext EntityManager.
    // On ne peut pas mocker EntityManager directement (pas un bean CDI standard).
    //
    // Alternative viable : atteindre le try block (non ADMIN_ORGANISATION) et tester
    // que la RuntimeException (L556) est bien wrappée.
    //
    // Note : les tests existants MembreServiceSearchAdvancedBranchesTest.java couvrent déjà
    // le try block en happy path. Pour déclencher le catch, il faudrait un mock d'EntityManager.
    // Ce fichier documente la contrainte et teste les comportements adjacents.
    // =========================================================================

    /**
     * Couvre le cas où le service organisationService lève une exception pour ADMIN_ORGANISATION
     * (avant le try block), vérifiant que l'exception est bien propagée.
     *
     * <p>Note : cela ne couvre pas L554-556 (catch du try block) mais teste la propagation
     * d'exception dans searchMembresAdvanced pour un chemin adjacent.
     */
    @Test
    @DisplayName("searchMembresAdvanced — ADMIN_ORGANISATION + listerOrgs lève exception → propagée")
    void searchMembresAdvanced_adminOrg_listerOrgsLanceException_propagee() {
        Principal principal = () -> "orgadmin.ex@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(organisationService.listerOrganisationsPourUtilisateur("orgadmin.ex@test.dev"))
                .thenThrow(new RuntimeException("Service org indisponible"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("test-exception")
                .build();

        // L'exception lancée avant le try block est propagée hors de searchMembresAdvanced
        assertThatThrownBy(() -> membreService.searchMembresAdvanced(
                criteria, Page.of(0, 10), Sort.by("nom")))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Service org indisponible");
    }

    /**
     * Vérifie que searchMembresAdvanced exécute bien le try block (L493+) sans erreur
     * quand l'utilisateur est SUPER_ADMIN et les critères ne donnent aucun résultat.
     *
     * <p>Le try block (L493-553) est exécuté → les lignes 493-553 sont parcourues.
     * Cela couvre l'exécution du try block en préparation pour L554-556.
     */
    @Test
    @DisplayName("searchMembresAdvanced — try block exécuté sans erreur (SUPER_ADMIN, 0 résultats)")
    void searchMembresAdvanced_superAdmin_tryBlockExecute_resultatVide() {
        Principal principal = () -> "super.admin.tryblock@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_INEXISTANT_L554_" + UUID.randomUUID())
                .build();

        MembreSearchResultDTO result = membreService.searchMembresAdvanced(
                criteria, Page.of(0, 10), Sort.by("nom"));

        assertThat(result).isNotNull();
        assertThat(result.getTotalElements()).isEqualTo(0);
    }

    // =========================================================================
    // L653 : return "m.nom ASC" dans buildOrderByClause quand sort.getColumns().isEmpty()
    //
    // Code source (L651-654) :
    //   private String buildOrderByClause(Sort sort) {
    //     if (sort == null || sort.getColumns().isEmpty()) {  ← L652 (condition)
    //       return "m.nom ASC";                               ← L653 (branche true côté isEmpty)
    //     }
    //     ...
    //   }
    //
    // buildOrderByClause est invoquée dans searchMembresAdvanced uniquement si sort != null (L519) :
    //   if (sort != null) {
    //     finalQuery += " ORDER BY " + buildOrderByClause(sort);
    //   }
    //
    // Pour atteindre L653 via sort.getColumns().isEmpty() = true avec sort != null :
    // on passe un Sort mocké Mockito dont getColumns() retourne une liste vide.
    //
    // Les tests existants passent toujours Sort.by("nom") qui a des colonnes (isEmpty = false).
    // =========================================================================

    /**
     * Couvre la ligne 653 de MembreService : {@code return "m.nom ASC";}
     *
     * <p>Condition : {@code sort != null} (vrai car sort est le mock) ET
     * {@code sort.getColumns().isEmpty()} = true (mock retourne liste vide).
     * → La branche {@code sort.getColumns().isEmpty()} = true est atteinte → L653 exécutée.
     *
     * <p>Le tri appliqué sera "m.nom ASC" (valeur par défaut) → la requête JPQL est valide
     * et le résultat est vide car la query utilise un terme inexistant.
     */
    @Test
    @DisplayName("Sort non-null avec getColumns() vide → buildOrderByClause retourne 'm.nom ASC' (L653)")
    void searchMembresAdvanced_sortAvecColonnesVides_defaultOrderByNomAsc() {
        Principal principal = () -> "user.sorttest@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L653_EMPTY_SORT_" + UUID.randomUUID())
                .build();

        // Sort mocké : sort != null ET sort.getColumns().isEmpty() = true
        // → buildOrderByClause entre dans la branche if → L653 atteinte
        Sort mockSort = mock(Sort.class);
        when(mockSort.getColumns()).thenReturn(Collections.emptyList());

        // La requête JPQL générée sera : SELECT ... WHERE ... ORDER BY m.nom ASC
        // C'est une requête JPQL valide → s'exécute sans erreur → résultat vide (terme inexistant)
        MembreSearchResultDTO result = membreService.searchMembresAdvanced(
                criteria, Page.of(0, 10), mockSort);

        assertThat(result).isNotNull();
        // Résultat vide car terme inexistant, mais L653 a bien été exécutée sans erreur
        assertThat(result.getTotalElements()).isEqualTo(0);
    }

    /**
     * Variante : Sort mocké avec getColumns() retournant null aurait causé une NPE dans
     * {@code sort.getColumns().isEmpty()}. Ce test vérifie le comportement avec une liste vide
     * (pas null) pour être sûr de déclencher la condition isEmpty().
     */
    @Test
    @DisplayName("Sort mocké getColumns() liste vide → 'm.nom ASC' utilisé → résultat valide (L653)")
    void searchMembresAdvanced_sortMockeColonnesVides_requeteValide() {
        Principal principal = () -> "user.sorttest2@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));

        // Critère de recherche avec nom spécifique pour exercer la clause ORDER BY nom ASC
        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .nom("zzz_L653_NOM_" + UUID.randomUUID())
                .build();

        Sort mockSort = mock(Sort.class);
        when(mockSort.getColumns()).thenReturn(Collections.emptyList());

        MembreSearchResultDTO result = membreService.searchMembresAdvanced(
                criteria, Page.of(0, 5), mockSort);

        assertThat(result).isNotNull();
        assertThat(result.getTotalElements()).isGreaterThanOrEqualTo(0);
    }

    // =========================================================================
    // L217-224 : getOrganisationIdsForCurrentUserIfAdminOrg — branches manquantes
    // =========================================================================

    @Test
    @DisplayName("getOrganisationIdsForCurrentUserIfAdminOrg — roles null → Optional.empty (L219)")
    void searchMembresAdvanced_rolesNull_getOrgIdsRetourneEmpty() {
        Principal principal = () -> "rolenull.test@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        // roles == null → L219: if (roles == null) return Optional.empty()
        when(securityIdentity.getRoles()).thenReturn(null);

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_ROLES_NULL_" + UUID.randomUUID())
                .build();

        MembreSearchResultDTO result = membreService.searchMembresAdvanced(
                criteria, Page.of(0, 10), Sort.by("nom"));

        assertThat(result).isNotNull();
        assertThat(result.getTotalElements()).isEqualTo(0);
    }

    @Test
    @DisplayName("getOrganisationIdsForCurrentUserIfAdminOrg — principal null → Optional.empty (L217)")
    void searchMembresAdvanced_principalNull_getOrgIdsRetourneEmpty() {
        // securityIdentity.getPrincipal() == null → L217: return Optional.empty()
        when(securityIdentity.getPrincipal()).thenReturn(null);

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_PRINCIPAL_NULL_" + UUID.randomUUID())
                .build();

        MembreSearchResultDTO result = membreService.searchMembresAdvanced(
                criteria, Page.of(0, 10), Sort.by("nom"));

        assertThat(result).isNotNull();
        assertThat(result.getTotalElements()).isEqualTo(0);
    }

    @Test
    @DisplayName("getOrganisationIdsForCurrentUserIfAdminOrg — ADMIN_ORGANISATION avec email blank → Optional.empty (L224)")
    void searchMembresAdvanced_adminOrgEmailBlank_getOrgIdsRetourneEmpty() {
        // email blank → L224: if (email == null || email.isBlank()) return Optional.empty()
        Principal principal = () -> "   "; // isBlank() = true
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_EMAIL_BLANK_" + UUID.randomUUID())
                .build();

        MembreSearchResultDTO result = membreService.searchMembresAdvanced(
                criteria, Page.of(0, 10), Sort.by("nom"));

        assertThat(result).isNotNull();
        assertThat(result.getTotalElements()).isEqualTo(0);
    }
}
