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.Mockito.when;

import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.Role;
import dev.lions.unionflow.server.entity.MembreRole;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.MembreRoleRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import java.security.Principal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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.Nested;
import org.junit.jupiter.api.Test;

/**
 * Tests ciblant les méthodes à couverture insuffisante dans {@link MembreService} :
 * <ul>
 *   <li>{@code exporterMembresSelectionnes} — ligne 779 (9I, 6B)</li>
 *   <li>{@code lambda$convertToSummaryResponse$2} — ligne 343 (1I)</li>
 * </ul>
 *
 * <p>Ces méthodes ne sont pas pleinement couvertes par les tests existants.
 */
@QuarkusTest
@DisplayName("MembreService — exporterMembresSelectionnes et convertToSummaryResponse lambda")
class MembreServiceExportAndSummaryTest {

    @Inject
    MembreService membreService;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    MembreRoleRepository membreRoleRepository;

    @InjectMock
    TypeReferenceRepository typeReferenceRepository;

    @InjectMock
    MembreImportExportService membreImportExportService;

    @InjectMock
    OrganisationService organisationService;

    @InjectMock
    SecurityIdentity securityIdentity;

    @BeforeEach
    void setupSecurity() {
        Principal principal = () -> "anonymous";
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(securityIdentity.getRoles()).thenReturn(Set.of());
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    private Membre membreFixture(String email) {
        Membre m = new Membre();
        m.setId(UUID.randomUUID());
        m.setEmail(email);
        m.setNom("Test");
        m.setPrenom("User");
        m.setNumeroMembre("UF-TEST-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
        m.setDateNaissance(LocalDate.of(1990, 1, 1));
        m.setActif(true);
        m.setStatutCompte("ACTIF");
        m.setVersion(0L);
        m.setMembresOrganisations(new ArrayList<>());
        m.setAdresses(new ArrayList<>());
        return m;
    }

    // =========================================================================
    // exporterMembresSelectionnes — branches manquantes
    // =========================================================================

    @Nested
    @DisplayName("exporterMembresSelectionnes")
    class ExporterMembresSelectionnesTests {

        @Test
        @DisplayName("Liste null → lève IllegalArgumentException")
        void exporterMembresSelectionnes_nullList_throwsIllegalArgument() {
            assertThatThrownBy(() -> membreService.exporterMembresSelectionnes(null, "CSV"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("ne peut pas être vide");
        }

        @Test
        @DisplayName("Liste vide → lève IllegalArgumentException")
        void exporterMembresSelectionnes_emptyList_throwsIllegalArgument() {
            assertThatThrownBy(() -> membreService.exporterMembresSelectionnes(List.of(), "CSV"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("ne peut pas être vide");
        }

        @Test
        @DisplayName("Un ID valide → retourne bytes CSV avec en-tête")
        void exporterMembresSelectionnes_oneValidId_returnsCsvBytes() {
            UUID id = UUID.randomUUID();
            Membre membre = membreFixture("export@test.dev");
            membre.setId(id);
            membre.setNumeroMembre("UF-EXPORT-001");
            membre.setNom("Diallo");
            membre.setPrenom("Aminata");
            membre.setStatutCompte("ACTIF");

            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre));
            when(membreRoleRepository.findActifsByMembreId(id)).thenReturn(List.of());

            byte[] result = membreService.exporterMembresSelectionnes(List.of(id), "CSV");

            assertThat(result).isNotNull();
            assertThat(result.length).isGreaterThan(0);

            String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8);
            assertThat(csv).contains("Numéro;Nom;Prénom;Email;Téléphone;Statut;Date Adhésion");
            assertThat(csv).contains("Diallo");
            assertThat(csv).contains("Aminata");
        }

        @Test
        @DisplayName("ID inexistant → filtré (opt.isPresent()=false), CSV retourné sans ce membre")
        void exporterMembresSelectionnes_unknownId_filteredOut() {
            UUID unknownId = UUID.randomUUID();
            when(membreRepository.findByIdOptional(unknownId)).thenReturn(Optional.empty());

            // Un ID inconnu est filtré → liste de membres vide → CSV avec juste l'en-tête
            byte[] result = membreService.exporterMembresSelectionnes(List.of(unknownId), "CSV");

            assertThat(result).isNotNull();
            String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8);
            // L'en-tête est toujours présent
            assertThat(csv).contains("Numéro;Nom;Prénom");
            // Aucune ligne de données pour le membre inconnu
            // String.lines() ne compte pas le '\n' terminal comme ligne vide
            assertThat(csv.lines().count()).isEqualTo(1L); // en-tête uniquement
        }

        @Test
        @DisplayName("Plusieurs IDs dont certains valides → CSV contient uniquement les membres trouvés")
        void exporterMembresSelectionnes_mixedIds_onlyFoundIncluded() {
            UUID idValide = UUID.randomUUID();
            UUID idInconnu = UUID.randomUUID();

            Membre membre = membreFixture("valide@test.dev");
            membre.setId(idValide);
            membre.setNom("Sow");
            membre.setPrenom("Moussa");
            membre.setNumeroMembre("UF-MIX-001");

            when(membreRepository.findByIdOptional(idValide)).thenReturn(Optional.of(membre));
            when(membreRepository.findByIdOptional(idInconnu)).thenReturn(Optional.empty());
            when(membreRoleRepository.findActifsByMembreId(idValide)).thenReturn(List.of());

            byte[] result = membreService.exporterMembresSelectionnes(List.of(idValide, idInconnu), "CSV");

            assertThat(result).isNotNull();
            String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8);
            assertThat(csv).contains("Sow");
            assertThat(csv).contains("Moussa");
        }

        @Test
        @DisplayName("Membre avec champs null → CSV généré sans NullPointerException")
        void exporterMembresSelectionnes_membreWithNullFields_noException() {
            UUID id = UUID.randomUUID();
            Membre membre = new Membre();
            membre.setId(id);
            // tous les champs optionnels à null
            membre.setNumeroMembre(null);
            membre.setNom(null);
            membre.setPrenom(null);
            membre.setEmail(null);
            membre.setTelephone(null);
            membre.setStatutCompte(null);
            membre.setActif(true);
            membre.setVersion(0L);
            membre.setMembresOrganisations(new ArrayList<>());
            membre.setAdresses(new ArrayList<>());

            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre));
            when(membreRoleRepository.findActifsByMembreId(id)).thenReturn(List.of());

            byte[] result = membreService.exporterMembresSelectionnes(List.of(id), "EXCEL");

            assertThat(result).isNotNull();
            String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8);
            // Les champs null sont remplacés par "" dans le format CSV
            assertThat(csv).contains(";;");
        }

        @Test
        @DisplayName("Format EXCEL (non CSV) → même logique CSV retournée")
        void exporterMembresSelectionnes_excelFormat_returnsCsvBytes() {
            UUID id = UUID.randomUUID();
            Membre membre = membreFixture("excel@test.dev");
            membre.setId(id);

            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre));
            when(membreRoleRepository.findActifsByMembreId(id)).thenReturn(List.of());

            byte[] result = membreService.exporterMembresSelectionnes(List.of(id), "EXCEL");

            assertThat(result).isNotNull();
            assertThat(result.length).isGreaterThan(0);
        }

        @Test
        @DisplayName("Membre avec dateAdhesion non null → incluse dans le CSV")
        void exporterMembresSelectionnes_membreAvecDateAdhesion_dateIncluse() {
            UUID id = UUID.randomUUID();
            Membre membre = membreFixture("adhesion@test.dev");
            membre.setId(id);

            // Ajouter un MembreOrganisation avec dateAdhesion
            Organisation org = new Organisation();
            org.setId(UUID.randomUUID());
            org.setNom("Org Test");

            MembreOrganisation mo = new MembreOrganisation();
            mo.setOrganisation(org);
            mo.setDateAdhesion(LocalDate.of(2023, 6, 15));

            membre.setMembresOrganisations(List.of(mo));

            when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre));
            when(membreRoleRepository.findActifsByMembreId(id)).thenReturn(List.of());

            byte[] result = membreService.exporterMembresSelectionnes(List.of(id), "CSV");

            assertThat(result).isNotNull();
            String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8);
            // dateAdhesion vient de MembreResponse.getDateAdhesion() → null si non mappé dans convertToResponse
            // Ce test vérifie qu'aucune exception n'est levée
            assertThat(csv).isNotBlank();
        }
    }

    // =========================================================================
    // lambda$convertToSummaryResponse$2 — ligne 343 (1I)
    // Branche : mo.getOrganisation() != null dans convertToSummaryResponse
    // =========================================================================

    @Nested
    @DisplayName("convertToSummaryResponse — lambda ligne 343 (mo.getOrganisation() != null)")
    class ConvertToSummaryResponseLambdaTests {

        @Test
        @DisplayName("MembreOrganisation avec organisation non null → organisationId et nom remplis")
        void convertToSummaryResponse_moAvecOrganisationNonNull_rempliOrgIdEtNom() {
            UUID orgId = UUID.randomUUID();
            Organisation org = new Organisation();
            org.setId(orgId);
            org.setNom("Lions Club Dakar");

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

            Membre membre = membreFixture("summary-org@test.dev");
            membre.setMembresOrganisations(List.of(mo));

            when(membreRoleRepository.findActifsByMembreId(membre.getId())).thenReturn(List.of());

            dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse resp =
                    membreService.convertToSummaryResponse(membre);

            assertThat(resp).isNotNull();
            // Branche mo.getOrganisation() != null → true → organisationId et organisationNom remplis
            assertThat(resp.getOrganisationId()).isEqualTo(orgId);
            assertThat(resp.getOrganisationNom()).isEqualTo("Lions Club Dakar");
        }

        @Test
        @DisplayName("MembreOrganisation avec organisation null → organisationId null (branche false ligne 343)")
        void convertToSummaryResponse_moAvecOrganisationNull_orgIdNull() {
            MembreOrganisation mo = new MembreOrganisation();
            mo.setOrganisation(null);  // ← déclenche la branche false de "mo.getOrganisation() != null"

            Membre membre = membreFixture("summary-nullorg@test.dev");
            membre.setMembresOrganisations(List.of(mo));

            when(membreRoleRepository.findActifsByMembreId(membre.getId())).thenReturn(List.of());

            dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse resp =
                    membreService.convertToSummaryResponse(membre);

            assertThat(resp).isNotNull();
            // Branche false → organisationId et organisationNom restent null
            assertThat(resp.getOrganisationId()).isNull();
            assertThat(resp.getOrganisationNom()).isNull();
        }

        @Test
        @DisplayName("Liste membresOrganisations vide → organisationId null (pas d'entrée dans le bloc if)")
        void convertToSummaryResponse_membresOrganisationsVide_orgIdNull() {
            Membre membre = membreFixture("summary-empty-org@test.dev");
            membre.setMembresOrganisations(new ArrayList<>());

            when(membreRoleRepository.findActifsByMembreId(membre.getId())).thenReturn(List.of());

            dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse resp =
                    membreService.convertToSummaryResponse(membre);

            assertThat(resp).isNotNull();
            assertThat(resp.getOrganisationId()).isNull();
            assertThat(resp.getOrganisationNom()).isNull();
        }

        @Test
        @DisplayName("Liste membresOrganisations null → organisationId null (condition isNotEmpty)")
        void convertToSummaryResponse_membresOrganisationsNull_orgIdNull() {
            Membre membre = membreFixture("summary-null-list@test.dev");
            membre.setMembresOrganisations(null);

            when(membreRoleRepository.findActifsByMembreId(membre.getId())).thenReturn(List.of());

            dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse resp =
                    membreService.convertToSummaryResponse(membre);

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

        @Test
        @DisplayName("MembreRole avec rôle non null → code inclus dans la liste des rôles")
        void convertToSummaryResponse_roleNonNull_codeInclus() {
            Role role = new Role();
            role.setCode("SECRETAIRE");

            MembreRole mr = new MembreRole();
            mr.setRole(role);

            Membre membre = membreFixture("summary-role@test.dev");
            membre.setMembresOrganisations(new ArrayList<>());

            when(membreRoleRepository.findActifsByMembreId(membre.getId())).thenReturn(List.of(mr));

            dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse resp =
                    membreService.convertToSummaryResponse(membre);

            assertThat(resp.getRoles()).contains("SECRETAIRE");
        }

        @Test
        @DisplayName("MembreRole avec rôle null → filtré, liste rôles vide")
        void convertToSummaryResponse_roleNull_filtre() {
            MembreRole mr = new MembreRole();
            mr.setRole(null); // rôle null → filtré par le stream filter

            Membre membre = membreFixture("summary-nullrole@test.dev");
            membre.setMembresOrganisations(new ArrayList<>());

            when(membreRoleRepository.findActifsByMembreId(membre.getId())).thenReturn(List.of(mr));

            dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse resp =
                    membreService.convertToSummaryResponse(membre);

            assertThat(resp.getRoles()).isEmpty();
        }
    }
}
