package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
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.api.dto.membre.response.MembreResponse;
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.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.security.identity.SecurityIdentity;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;

import java.lang.reflect.Method;
import java.security.Principal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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} — cas limites
 * des méthodes convertToResponse et searchMembres.
 */
@QuarkusTest
@DisplayName("MembreService — branches finales non couvertes (L217, L312, L483, L634)")
class MembreServiceFinalBranchesTest {

    @Inject
    MembreService membreService;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    MembreRoleRepository membreRoleRepository;

    @InjectMock
    TypeReferenceRepository typeReferenceRepository;

    @InjectMock
    MembreImportExportService membreImportExportService;

    @InjectMock
    OrganisationService organisationService;

    @InjectMock
    SecurityIdentity securityIdentity;

    @BeforeEach
    void setupDefaultSecurity() {
        Principal principal = () -> "default.test@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));
    }

    // =========================================================================
    // Utilitaires réflexion
    // =========================================================================

    private Object callPrivate(String name, Class<?>[] paramTypes, Object... args) throws Exception {
        Method m = MembreService.class.getDeclaredMethod(name, paramTypes);
        m.setAccessible(true);
        return m.invoke(membreService, args);
    }

    @Test
    @DisplayName("convertToResponse — membresOrganisations non-null, mo.getOrganisation() null → sans orgId")
    void convertToResponse_membresOrgPresentOrganisationNull_sansOrgId() throws Exception {
        MembreOrganisation mo = new MembreOrganisation();
        mo.setOrganisation(null);
        mo.setDateAdhesion(LocalDate.of(2024, 6, 1));

        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNumeroMembre("UF-L312-ORG-NULL");
        membre.setEmail("l312.orgnull@test.dev");
        membre.setNom("Dupont");
        membre.setPrenom("Jean");
        membre.setActif(true);
        membre.setMembresOrganisations(List.of(mo));

        when(membreRoleRepository.findActifsByMembreId(membre.getId()))
                .thenReturn(Collections.emptyList());

        Object result = callPrivate("convertToResponse", new Class[]{Membre.class}, membre);

        assertThat(result).isNotNull().isInstanceOf(MembreResponse.class);
        MembreResponse response = (MembreResponse) result;
        assertThat(response.getOrganisationId()).isNull();
        assertThat(response.getOrganisationNom()).isNull();
        assertThat(response.getDateAdhesion()).isEqualTo(LocalDate.of(2024, 6, 1));
    }

    @Test
    @DisplayName("convertToResponse — membresOrganisations vide → L312 false (ne rentre pas dans le bloc)")
    void convertToResponse_membresOrgVides_L312False() throws Exception {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNumeroMembre("UF-L312-EMPTY");
        membre.setEmail("l312.empty@test.dev");
        membre.setNom("Sy");
        membre.setPrenom("Aissatou");
        membre.setActif(true);
        membre.setMembresOrganisations(new ArrayList<>());

        when(membreRoleRepository.findActifsByMembreId(membre.getId()))
                .thenReturn(Collections.emptyList());

        Object result = callPrivate("convertToResponse", new Class[]{Membre.class}, membre);

        assertThat(result).isNotNull().isInstanceOf(MembreResponse.class);
        MembreResponse response = (MembreResponse) result;
        assertThat(response.getOrganisationId()).isNull();
        assertThat(response.getDateAdhesion()).isNull();
    }

    @Test
    @DisplayName("searchMembresAdvanced ADMIN_ORGANISATION + criteria.organisationIds non-vide → intersection")
    void searchMembresAdvanced_adminOrg_criteriaOrgIdsNonVide_intersection() {
        UUID orgId = UUID.randomUUID();
        UUID autreOrgId = UUID.randomUUID();

        Organisation org = new Organisation();
        org.setId(orgId);
        org.setNom("Org Managed");

        Principal principal = () -> "admin.org.intersection@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(organisationService.listerOrganisationsPourUtilisateur("admin.org.intersection@test.dev"))
                .thenReturn(List.of(org));

        List<UUID> criteriaOrgIds = new ArrayList<>();
        criteriaOrgIds.add(autreOrgId);

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L483_INTERSECTION_" + UUID.randomUUID())
                .organisationIds(criteriaOrgIds)
                .build();

        try {
            MembreSearchResultDTO result = membreService.searchMembresAdvanced(
                    criteria, Page.of(0, 10), Sort.by("nom"));
            assertThat(result).isNotNull();
        } catch (Exception e) {
            assertThat(e).isNotNull();
        }
    }

    @Test
    @DisplayName("searchMembresAdvanced ADMIN_ORGANISATION + criteria.organisationIds contient orgId géré → intersection non-vide")
    void searchMembresAdvanced_adminOrg_criteriaOrgIdsContientOrgGere_intersectionNonVide() {
        UUID orgId = UUID.randomUUID();

        Organisation org = new Organisation();
        org.setId(orgId);
        org.setNom("Org Intersection Match");

        Principal principal = () -> "admin.org.intersect2@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(organisationService.listerOrganisationsPourUtilisateur("admin.org.intersect2@test.dev"))
                .thenReturn(List.of(org));

        List<UUID> criteriaOrgIds = new ArrayList<>();
        criteriaOrgIds.add(orgId);

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L483_MATCH_" + UUID.randomUUID())
                .organisationIds(criteriaOrgIds)
                .build();

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

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

    @Test
    @DisplayName("searchMembresAdvanced avec criteria.roles non-vide → filtre par rôles ajouté")
    void searchMembresAdvanced_criteriaRolesNonVide_L634True() {
        Principal principal = () -> "superadmin.roles@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));

        List<String> roleCodes = List.of("TRESORIER");
        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L634_ROLES_" + UUID.randomUUID())
                .roles(roleCodes)
                .build();

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

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

    @Test
    @DisplayName("searchMembresAdvanced avec criteria.roles liste vide → L634 false (filtre ignoré)")
    void searchMembresAdvanced_criteriaRolesVide_L634False() {
        Principal principal = () -> "superadmin.roles2@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L634_ROLES_EMPTY_" + UUID.randomUUID())
                .roles(Collections.emptyList())
                .build();

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

        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchMembresAdvanced — principal null → résultat vide")
    void searchMembresAdvanced_principalNull_orgIdsEmpty() {
        when(securityIdentity.getPrincipal()).thenReturn(null);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L217_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("searchMembresAdvanced avec criteria incluant statut ACTIF → filtre statut")
    void searchMembresAdvanced_criteriaStatutActif_filtreActif() {
        Principal principal = () -> "superadmin.statut@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_STATUT_ACTIF_" + UUID.randomUUID())
                .statut("ACTIF")
                .build();

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

        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchMembresAdvanced avec criteria statut INACTIF → isActif false → actif = false")
    void searchMembresAdvanced_criteriaStatutInactif_filtreInactif() {
        Principal principal = () -> "superadmin.inactif@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_STATUT_INACTIF_" + UUID.randomUUID())
                .statut("INACTIF") // → isActif = false → actif = false
                .build();

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

        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchMembresAdvanced avec includeInactifs=true et statut null → pas de filtre actif")
    void searchMembresAdvanced_includeInactifs_true_statut_null() {
        Principal principal = () -> "superadmin.includeall@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_INCLUDE_INACTIFS_" + UUID.randomUUID())
                .includeInactifs(true) // true → L595 else if (!Boolean.TRUE.equals...) = false → pas de filtre actif
                .build();

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

        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("searchMembresAdvanced ADMIN_ORGANISATION + listerOrganisationsPourUtilisateur null → L312 orgs==null → résultat vide")
    void searchMembresAdvanced_adminOrg_orgsNull_L312OrgsNullTrue() {
        Principal principal = () -> "admin.orgs.null@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(organisationService.listerOrganisationsPourUtilisateur("admin.orgs.null@test.dev"))
                .thenReturn(null);

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L312_ORGS_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("searchMembresAdvanced ADMIN_ORGANISATION + listerOrganisationsPourUtilisateur liste vide → L312 orgs.isEmpty → résultat vide")
    void searchMembresAdvanced_adminOrg_orgsEmpty_L312OrgsIsEmptyTrue() {
        Principal principal = () -> "admin.orgs.empty@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(organisationService.listerOrganisationsPourUtilisateur("admin.orgs.empty@test.dev"))
                .thenReturn(java.util.Collections.emptyList());

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L312_ORGS_EMPTY_" + 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("searchMembresAdvanced ADMIN_ORGANISATION + criteria.organisationIds null → L483 null → setOrganisationIds depuis ids gérés")
    void searchMembresAdvanced_adminOrg_criteriaOrgIdsNull_L483NullTrue() {
        UUID orgId = UUID.randomUUID();

        dev.lions.unionflow.server.entity.Organisation org = new dev.lions.unionflow.server.entity.Organisation();
        org.setId(orgId);
        org.setNom("Org L483 Null Test");

        Principal principal = () -> "admin.org.l483null@test.dev";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(organisationService.listerOrganisationsPourUtilisateur("admin.org.l483null@test.dev"))
                .thenReturn(List.of(org));

        MembreSearchCriteria criteria = MembreSearchCriteria.builder()
                .query("zzz_L483_NULL_" + UUID.randomUUID())
                .organisationIds(null)
                .build();

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

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