package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.CotisationRepository;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

/**
 * Tests de couverture pour les branches manquantes dans ExportService.
 *
 * <p>Utilise @InjectMock sur CotisationRepository pour contrôler précisément
 * les valeurs nulles retournées, couvrant ainsi les branches ternaires null-guards
 * que les tests d'intégration ne peuvent pas atteindre (les entités JPA ont
 * toujours leurs champs obligatoires renseignés en base).
 */
@QuarkusTest
class ExportServiceCoverageTest {

    @Inject
    ExportService exportService;

    @InjectMock
    CotisationRepository cotisationRepository;

    private UUID cotisationId;

    @BeforeEach
    void setUp() {
        cotisationId = UUID.randomUUID();
    }

    // =========================================================================
    // exporterCotisationsCSV — null branches (L48-L59)
    // =========================================================================

    /**
     * Couvre les branches NULL sur L48 (membre=null→""), L52 (numeroReference=null→""),
     * L54 (typeCotisation=null→""), L55 (montantDu=null→"0"), L56 (montantPaye=null→"0"),
     * L57 (statut=null→""), L58 (dateEcheance=null→""), L59 (datePaiement=null→"").
     */
    @Test
    @DisplayName("exporterCotisationsCSV — cotisation avec tous les champs null gère les branches null-guards")
    void exporterCotisationsCSV_champsNulls_branchesNullCouvertes() {
        Cotisation c = new Cotisation();
        // Tous les champs optionnels restent null
        c.setId(cotisationId);
        // membre=null → branche L48 false → ""
        // numeroReference=null → branche L52 false → ""
        // typeCotisation=null → branche L54 false → ""
        // montantDu=null → branche L55 false → "0"
        // montantPaye=null → branche L56 false → "0"
        // statut=null → branche L57 false → ""
        // dateEcheance=null → branche L58 false → ""
        // datePaiement=null → branche L59 false → ""

        when(cotisationRepository.findByIdOptional(cotisationId)).thenReturn(Optional.of(c));

        byte[] csv = exportService.exporterCotisationsCSV(List.of(cotisationId));
        String contenu = new String(csv, java.nio.charset.StandardCharsets.UTF_8);

        assertThat(contenu).isNotEmpty();
        assertThat(contenu).contains("Numéro Référence");
        // La ligne de données existe avec des champs vides / "0"
        assertThat(contenu).contains(";0;0;");
    }

    /**
     * Couvre la branche false de L46: {@code if (cotisationOpt.isPresent())} —
     * cotisationRepository.findByIdOptional() retourne Optional.empty() → le bloc est sauté.
     */
    @Test
    @DisplayName("exporterCotisationsCSV — cotisationId introuvable → Optional.empty() → branche L46 false (saut)")
    void exporterCotisationsCSV_cotisationIntrouvable_optionalEmpty_brancheL46False() {
        UUID idIntrouvable = UUID.randomUUID();
        // findByIdOptional retourne Optional.empty() → if (cotisationOpt.isPresent()) = false → bloc sauté
        when(cotisationRepository.findByIdOptional(idIntrouvable)).thenReturn(Optional.empty());

        byte[] csv = exportService.exporterCotisationsCSV(List.of(idIntrouvable));
        String contenu = new String(csv, java.nio.charset.StandardCharsets.UTF_8);

        // Seul le header est présent — aucune ligne de données ajoutée
        assertThat(contenu).contains("Numéro Référence");
        // Aucune ligne de données (juste le header)
        String[] lignes = contenu.split("\n");
        assertThat(lignes).hasSize(1); // seulement le header
    }

    /**
     * Couvre la branche L48 true : membre non-null → utilise nom + prénom.
     * Aussi couvre dateEcheance non-null et datePaiement non-null (branches true).
     */
    @Test
    @DisplayName("exporterCotisationsCSV — cotisation avec membre non-null et dates renseignées")
    void exporterCotisationsCSV_avecMembreEtDates_branchesTrue() {
        Membre membre = new Membre();
        membre.setNom("Dupont");
        membre.setPrenom("Marie");

        Cotisation c = new Cotisation();
        c.setId(cotisationId);
        c.setMembre(membre);
        c.setNumeroReference("REF-001");
        c.setTypeCotisation("ANNUELLE");
        c.setMontantDu(new BigDecimal("5000"));
        c.setMontantPaye(new BigDecimal("5000"));
        c.setStatut("PAYEE");
        c.setDateEcheance(LocalDate.of(2024, 12, 31));
        c.setDatePaiement(LocalDateTime.of(2024, 12, 15, 10, 30));

        when(cotisationRepository.findByIdOptional(cotisationId)).thenReturn(Optional.of(c));

        byte[] csv = exportService.exporterCotisationsCSV(List.of(cotisationId));
        String contenu = new String(csv, java.nio.charset.StandardCharsets.UTF_8);

        assertThat(contenu).contains("Dupont Marie");
        assertThat(contenu).contains("REF-001");
        assertThat(contenu).contains("ANNUELLE");
        assertThat(contenu).contains("PAYEE");
        assertThat(contenu).contains("31/12/2024");
        assertThat(contenu).contains("15/12/2024 10:30");
    }

    // =========================================================================
    // exporterToutesCotisationsCSV — null branches dans les lambdas de filtre
    // =========================================================================

    /**
     * Couvre la branche L76: statut="" (vide) → condition false → pas de filtre statut.
     * Couvre la branche L81: type="" (vide) → condition false → pas de filtre type.
     * (Les 4 branches: null&&empty: null=false|empty=true pour statut vide)
     */
    @Test
    @DisplayName("exporterToutesCotisationsCSV — statut et type vides (empty) n'appliquent pas de filtre")
    void exporterToutesCotisationsCSV_statutEtTypeVides_pasDeFiltre() {
        Cotisation c = new Cotisation();
        c.setId(UUID.randomUUID());
        c.setStatut("EN_ATTENTE");
        c.setTypeCotisation("ANNUELLE");

        when(cotisationRepository.listAll()).thenReturn(List.of(c));

        // statut="" et type="" → condition `statut != null && !statut.isEmpty()` → false → pas de filtre
        byte[] csv = exportService.exporterToutesCotisationsCSV("", "", null);
        String contenu = new String(csv, java.nio.charset.StandardCharsets.UTF_8);
        assertThat(contenu).contains("Numéro Référence");
    }

    /**
     * Couvre la branche L78: `c.getStatut() == null` → condition false dans le filtre statut.
     */
    @Test
    @DisplayName("exporterToutesCotisationsCSV — cotisation avec statut null exclue par le filtre")
    void exporterToutesCotisationsCSV_cotisationStatutNull_exclueParFiltre() {
        Cotisation cAvecStatutNull = new Cotisation();
        cAvecStatutNull.setId(UUID.randomUUID());
        cAvecStatutNull.setStatut(null); // statut null → filtre `c.getStatut() != null` = false → exclu

        Cotisation cAvecStatut = new Cotisation();
        UUID cId = UUID.randomUUID();
        cAvecStatut.setId(cId);
        cAvecStatut.setStatut("EN_ATTENTE");

        when(cotisationRepository.listAll()).thenReturn(List.of(cAvecStatutNull, cAvecStatut));
        when(cotisationRepository.findByIdOptional(cId)).thenReturn(Optional.of(cAvecStatut));

        byte[] csv = exportService.exporterToutesCotisationsCSV("EN_ATTENTE", null, null);
        assertThat(csv).isNotEmpty();
    }

    /**
     * Couvre la branche L83: `c.getTypeCotisation() == null` → condition false dans le filtre type.
     */
    @Test
    @DisplayName("exporterToutesCotisationsCSV — cotisation avec typeCotisation null exclue par le filtre type")
    void exporterToutesCotisationsCSV_cotisationTypeNull_exclueParFiltreType() {
        Cotisation cAvecTypeNull = new Cotisation();
        cAvecTypeNull.setId(UUID.randomUUID());
        cAvecTypeNull.setTypeCotisation(null); // type null → filtre `c.getTypeCotisation() != null` = false → exclu

        when(cotisationRepository.listAll()).thenReturn(List.of(cAvecTypeNull));

        byte[] csv = exportService.exporterToutesCotisationsCSV(null, "ANNUELLE", null);
        // La cotisation avec type null est exclue par le filtre → liste vide → seulement le header
        String contenu = new String(csv, java.nio.charset.StandardCharsets.UTF_8);
        assertThat(contenu).contains("Numéro Référence");
    }

    // =========================================================================
    // genererRecuPaiement — membre null (L119 branch false)
    // =========================================================================

    /**
     * Couvre la branche L119 false : `c.getMembre() == null` → le bloc "INFORMATIONS MEMBRE"
     * n'affiche pas le nom/prénom/numéro.
     * Aussi couvre L129,L131,L136,L137 branches null (typeCotisation, periode, datePaiement, statut).
     */
    @Test
    @DisplayName("genererRecuPaiement — cotisation sans membre ni champs optionnels couvre les null-guards")
    void genererRecuPaiement_sansMembreEtChampsNulls_couvreNullGuards() {
        Cotisation c = new Cotisation();
        c.setId(cotisationId);
        c.setNumeroReference("REF-SANS-MEMBRE");
        // membre=null → branche L119 false → bloc non affiché
        // typeCotisation=null → L129 false → ""
        // periode=null → L131 false → ""
        // datePaiement=null → L136 false → ""
        // statut=null → L137 false → ""
        // montantDu et montantPaye = null → formatMontant(null) → "0 FCFA" (L239-240)

        when(cotisationRepository.findByIdOptional(cotisationId)).thenReturn(Optional.of(c));

        byte[] recu = exportService.genererRecuPaiement(cotisationId);
        String contenu = new String(recu, java.nio.charset.StandardCharsets.UTF_8);

        assertThat(contenu).contains("REÇU DE PAIEMENT");
        assertThat(contenu).contains("REF-SANS-MEMBRE");
        // Le nom du membre n'apparaît pas
        assertThat(contenu).doesNotContain("Nom               :");
        // formatMontant(null) → "0 FCFA"
        assertThat(contenu).contains("0 FCFA");
    }

    // =========================================================================
    // genererRapportMensuel — branches lambda null (L179, L194, L199, L202)
    // =========================================================================

    /**
     * Couvre la branche L179: `c.getDateCreation() == null` → return false (nc ligne 180).
     * La cotisation avec dateCreation=null est exclue du rapport.
     */
    @Test
    @DisplayName("genererRapportMensuel — cotisation avec dateCreation null exclue (lambda L179 branche false)")
    void genererRapportMensuel_cotisationDateCreationNull_exclue() {
        Cotisation cSansDate = new Cotisation();
        cSansDate.setId(UUID.randomUUID());
        // dateCreation=null → lambda L179: if (null) return false → exclu du stream

        when(cotisationRepository.listAll()).thenReturn(List.of(cSansDate));

        byte[] rapport = exportService.genererRapportMensuel(2024, 1, null);
        String contenu = new String(rapport, java.nio.charset.StandardCharsets.UTF_8);
        assertThat(contenu).contains("RAPPORT MENSUEL");
        // 0 cotisations dans le rapport
        assertThat(contenu).contains("Total cotisations     : 0");
    }

    /**
     * Couvre la branche L194: `c.getMontantDu() == null` → BigDecimal.ZERO dans la map.
     * Couvre la branche L199: `c.getMontantPaye() == null` → BigDecimal.ZERO pour PAYEE.
     * Couvre la branche L202: `montantTotal == 0` → tauxRecouvrement = 0 (nc ligne 205).
     */
    @Test
    @DisplayName("genererRapportMensuel — cotisation PAYEE avec montants null couvre L194/L199/L202")
    void genererRapportMensuel_cotisationPayeeMontantsNulls_couvreL194L199L202() {
        // montantDu=null → L194: null → BigDecimal.ZERO → montantTotal=0
        // statut=PAYEE → filtre L198 pass → montantPaye=null → L199: null → BigDecimal.ZERO
        // montantTotal=0 → L202: compareTo(ZERO) > 0 = false → tauxRecouvrement=0 (L205 nc)
        Cotisation c = new Cotisation();
        c.setId(UUID.randomUUID());
        c.setStatut("PAYEE");
        c.setMontantDu(null);    // → BigDecimal.ZERO via L194 null-guard
        c.setMontantPaye(null);  // → BigDecimal.ZERO via L199 null-guard
        // dateCreation doit être dans la période pour être incluse
        java.time.LocalDateTime dateCreation = java.time.LocalDateTime.of(2024, 6, 15, 10, 0);
        c.setDateCreation(dateCreation);

        when(cotisationRepository.listAll()).thenReturn(List.of(c));

        byte[] rapport = exportService.genererRapportMensuel(2024, 6, null);
        String contenu = new String(rapport, java.nio.charset.StandardCharsets.UTF_8);

        assertThat(contenu).contains("RAPPORT MENSUEL");
        // montantTotal=0 → tauxRecouvrement=0.0%
        assertThat(contenu).contains("Taux de recouvrement  : 0,0%");
    }

    /**
     * Couvre la branche L182 (2 branches manquantes):
     * - dateCot.isBefore(debut) = true → exclu
     * - dateCot.isAfter(fin) = true → exclu
     */
    @Test
    @DisplayName("genererRapportMensuel — cotisations hors période exclues par lambda L182")
    void genererRapportMensuel_cotisationsHorsPeriode_exclues() {
        // Cotisation avant la période
        Cotisation cAvant = new Cotisation();
        cAvant.setId(UUID.randomUUID());
        cAvant.setDateCreation(java.time.LocalDateTime.of(2024, 5, 31, 23, 59)); // avant juin 2024

        // Cotisation après la période
        Cotisation cApres = new Cotisation();
        cApres.setId(UUID.randomUUID());
        cApres.setDateCreation(java.time.LocalDateTime.of(2024, 7, 1, 0, 0)); // après juin 2024

        when(cotisationRepository.listAll()).thenReturn(List.of(cAvant, cApres));

        byte[] rapport = exportService.genererRapportMensuel(2024, 6, null);
        String contenu = new String(rapport, java.nio.charset.StandardCharsets.UTF_8);

        assertThat(contenu).contains("Total cotisations     : 0");
    }

    // =========================================================================
    // formatMontant(null) — L239-240 (nc)
    // =========================================================================

    /**
     * Couvre la branche L239 true : `montant == null` → retourne "0 FCFA" (L240 nc).
     * Cette branche est atteinte en passant null à formatMontant via genererRecuPaiement
     * quand c.getMontantDu() == null ET c.getMontantPaye() == null.
     */
    @Test
    @DisplayName("formatMontant(null) retourne '0 FCFA' — couvre L239-240 via genererRecuPaiement")
    void formatMontant_null_retourneZeroFcfa() {
        Cotisation c = new Cotisation();
        c.setId(cotisationId);
        c.setNumeroReference("REF-FORMAT");
        c.setMontantDu(null);   // → formatMontant(null) → "0 FCFA"
        c.setMontantPaye(null); // → formatMontant(null) → "0 FCFA"

        when(cotisationRepository.findByIdOptional(cotisationId)).thenReturn(Optional.of(c));

        byte[] recu = exportService.genererRecuPaiement(cotisationId);
        String contenu = new String(recu, java.nio.charset.StandardCharsets.UTF_8);

        // formatMontant(null) → "0 FCFA"
        assertThat(contenu).contains("0 FCFA");
    }
}
