package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable;
import dev.lions.unionflow.server.entity.CompteComptable;
import dev.lions.unionflow.server.entity.EcritureComptable;
import dev.lions.unionflow.server.entity.LigneEcriture;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.CompteComptableRepository;
import dev.lions.unionflow.server.repository.EcritureComptableRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import jakarta.ws.rs.NotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

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

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

@ExtendWith(MockitoExtension.class)
@DisplayName("Tests ComptabilitePdfService")
class ComptabilitePdfServiceTest {

    @Mock
    OrganisationRepository organisationRepository;

    @Mock
    CompteComptableRepository compteComptableRepository;

    @Mock
    EcritureComptableRepository ecritureComptableRepository;

    @InjectMocks
    ComptabilitePdfService service;

    private static final UUID ORG_ID = UUID.randomUUID();
    private static final LocalDate DATE_DEBUT = LocalDate.of(2026, 1, 1);
    private static final LocalDate DATE_FIN = LocalDate.of(2026, 3, 31);

    private Organisation org;

    @BeforeEach
    void setUp() {
        org = new Organisation();
        org.setNom("Association Test");
    }

    // ─── Helper builders ────────────────────────────────────────────────────────

    private CompteComptable buildCompte(String numero, String libelle, int classe,
                                         TypeCompteComptable type) {
        CompteComptable c = new CompteComptable();
        c.setNumeroCompte(numero);
        c.setLibelle(libelle);
        c.setClasseComptable(classe);
        c.setTypeCompte(type);
        return c;
    }

    private EcritureComptable buildEcriture(LocalDate date, String piece, String libelle,
                                             List<LigneEcriture> lignes) {
        EcritureComptable e = new EcritureComptable();
        e.setDateEcriture(date);
        e.setNumeroPiece(piece);
        e.setLibelle(libelle);
        e.setLignes(lignes);
        return e;
    }

    private LigneEcriture buildLigne(CompteComptable compte, BigDecimal debit, BigDecimal credit) {
        LigneEcriture l = new LigneEcriture();
        l.setCompteComptable(compte);
        l.setMontantDebit(debit);
        l.setMontantCredit(credit);
        return l;
    }

    // ─── genererBalance ────────────────────────────────────────────────────────

    @Nested
    @DisplayName("genererBalance")
    class GenererBalanceTests {

        @Test
        @DisplayName("throws NotFoundException when organisation not found")
        void orgNotFound_throws() {
            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.empty());

            assertThatThrownBy(() -> service.genererBalance(ORG_ID, DATE_DEBUT, DATE_FIN))
                    .isInstanceOf(NotFoundException.class)
                    .hasMessageContaining("Organisation introuvable");
        }

        @Test
        @DisplayName("returns non-empty PDF bytes with no comptes")
        void noComptes_returnsPdfBytes() {
            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID))
                    .thenReturn(Collections.emptyList());
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(Collections.emptyList());

            byte[] result = service.genererBalance(ORG_ID, DATE_DEBUT, DATE_FIN);

            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("skips compte with zero debit and credit (both zero signum)")
        void compteWithZeroMovements_skipped() {
            CompteComptable compte = buildCompte("411000", "Clients", 4, TypeCompteComptable.ACTIF);

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID))
                    .thenReturn(List.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(Collections.emptyList()); // no movements → zero totals

            byte[] result = service.genererBalance(ORG_ID, DATE_DEBUT, DATE_FIN);

            // Should generate a valid PDF (TOTAUX row only)
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("generates PDF with compte having non-zero debit — alternating row colors")
        void compteWithMovements_generatesPdf() {
            CompteComptable compte1 = buildCompte("411000", "Clients", 4, TypeCompteComptable.ACTIF);
            CompteComptable compte2 = buildCompte("512000", "Banque", 5, TypeCompteComptable.TRESORERIE);

            LigneEcriture ligne1 = buildLigne(compte1, new BigDecimal("10000"), BigDecimal.ZERO);
            LigneEcriture ligne2 = buildLigne(compte2, BigDecimal.ZERO, new BigDecimal("5000"));

            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Paiement",
                    List.of(ligne1, ligne2));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID))
                    .thenReturn(List.of(compte1, compte2));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererBalance(ORG_ID, DATE_DEBUT, DATE_FIN);

            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("ecriture with null lignes — skipped in totaux calculation")
        void ecritureWithNullLignes_skipped() {
            CompteComptable compte = buildCompte("411000", "Clients", 4, TypeCompteComptable.ACTIF);

            EcritureComptable ecritureNullLignes = buildEcriture(DATE_DEBUT, "ECR-001",
                    "Test", null);

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID))
                    .thenReturn(List.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecritureNullLignes));

            byte[] result = service.genererBalance(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("ligne with null compteComptable — skipped in totaux calculation")
        void ligneWithNullCompte_skipped() {
            CompteComptable compte = buildCompte("411000", "Clients", 4, TypeCompteComptable.ACTIF);

            LigneEcriture ligneNullCompte = buildLigne(null, new BigDecimal("1000"), null);
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Test",
                    List.of(ligneNullCompte));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID))
                    .thenReturn(List.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererBalance(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("ligne with null debit and null credit — treated as zero")
        void ligneWithNullAmounts_treatedAsZero() {
            CompteComptable compte = buildCompte("411000", "Clients", 4, TypeCompteComptable.ACTIF);
            LigneEcriture ligne = buildLigne(compte, null, null); // null amounts
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Test",
                    List.of(ligne));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(List.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererBalance(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }
    }

    // ─── genererCompteResultat ─────────────────────────────────────────────────

    @Nested
    @DisplayName("genererCompteResultat")
    class GenererCompteResultatTests {

        @Test
        @DisplayName("throws NotFoundException when organisation not found")
        void orgNotFound_throws() {
            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.empty());

            assertThatThrownBy(() -> service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("empty comptes — returns PDF bytes (bénéfice nul)")
        void noComptes_returnsPdf() {
            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(Collections.emptyList());
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(Collections.emptyList());

            byte[] result = service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("classe 7 compte (produits) with non-zero solde — added to produits section")
        void classe7Compte_addedToProduits() {
            CompteComptable produit = buildCompte("700000", "Ventes", 7, TypeCompteComptable.PRODUITS);
            LigneEcriture ligne = buildLigne(produit, BigDecimal.ZERO, new BigDecimal("50000"));
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Ventes", List.of(ligne));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(List.of(produit));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("classe 6 compte (charges) with non-zero solde — added to charges section")
        void classe6Compte_addedToCharges() {
            CompteComptable charge = buildCompte("600000", "Achats", 6, TypeCompteComptable.CHARGES);
            LigneEcriture ligne = buildLigne(charge, new BigDecimal("20000"), BigDecimal.ZERO);
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Charges", List.of(ligne));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(List.of(charge));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("classe 8 PRODUITS compte — included in produits section")
        void classe8ProduitCompte_addedToProduits() {
            CompteComptable produit8 = buildCompte("800000", "Produits exceptionnels", 8,
                    TypeCompteComptable.PRODUITS);
            LigneEcriture ligne = buildLigne(produit8, BigDecimal.ZERO, new BigDecimal("10000"));
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Prod except", List.of(ligne));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(List.of(produit8));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("classe 8 CHARGES compte — included in charges section")
        void classe8ChargesCompte_addedToCharges() {
            CompteComptable charges8 = buildCompte("800001", "Charges exceptionnelles", 8,
                    TypeCompteComptable.CHARGES);
            LigneEcriture ligne = buildLigne(charges8, new BigDecimal("5000"), BigDecimal.ZERO);
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Charges except",
                    List.of(ligne));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(List.of(charges8));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("perte nette — charges > produits — PDF uses red color for PERTE label")
        void perteNette_generatesLabelPerte() {
            CompteComptable charge = buildCompte("600000", "Achats", 6, TypeCompteComptable.CHARGES);
            CompteComptable produit = buildCompte("700000", "Ventes", 7, TypeCompteComptable.PRODUITS);

            LigneEcriture ligneCharge = buildLigne(charge, new BigDecimal("100000"), BigDecimal.ZERO);
            LigneEcriture ligneProduit = buildLigne(produit, BigDecimal.ZERO, new BigDecimal("20000"));

            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Mix",
                    List.of(ligneCharge, ligneProduit));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(List.of(charge, produit));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("zero-solde produit compte — skipped (no ligne added)")
        void zeroSoldeProduit_skipped() {
            CompteComptable produit = buildCompte("700000", "Ventes", 7, TypeCompteComptable.PRODUITS);
            // debit == credit → solde = 0, should be skipped
            LigneEcriture ligne = buildLigne(produit, new BigDecimal("5000"), new BigDecimal("5000"));
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Zero", List.of(ligne));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(List.of(produit));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("zero-solde charge compte — skipped (no ligne added)")
        void zeroSoldeCharge_skipped() {
            CompteComptable charge = buildCompte("600000", "Achats", 6, TypeCompteComptable.CHARGES);
            LigneEcriture ligne = buildLigne(charge, new BigDecimal("5000"), new BigDecimal("5000"));
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Zero", List.of(ligne));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(List.of(charge));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererCompteResultat(ORG_ID, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }
    }

    // ─── genererGrandLivre ─────────────────────────────────────────────────────

    @Nested
    @DisplayName("genererGrandLivre")
    class GenererGrandLivreTests {

        private static final String NUMERO_COMPTE = "411000";

        @Test
        @DisplayName("throws NotFoundException when organisation not found")
        void orgNotFound_throws() {
            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.empty());

            assertThatThrownBy(() -> service.genererGrandLivre(ORG_ID, NUMERO_COMPTE, DATE_DEBUT, DATE_FIN))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("throws NotFoundException when compte not found for organisation")
        void compteNotFound_throws() {
            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisationAndNumero(ORG_ID, NUMERO_COMPTE))
                    .thenReturn(Optional.empty());

            assertThatThrownBy(() -> service.genererGrandLivre(ORG_ID, NUMERO_COMPTE, DATE_DEBUT, DATE_FIN))
                    .isInstanceOf(NotFoundException.class)
                    .hasMessageContaining(NUMERO_COMPTE);
        }

        @Test
        @DisplayName("no écritures — returns PDF with empty movement cell")
        void noEcritures_pdfWithEmptyMessage() {
            CompteComptable compte = buildCompte(NUMERO_COMPTE, "Clients", 4, TypeCompteComptable.ACTIF);

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisationAndNumero(ORG_ID, NUMERO_COMPTE))
                    .thenReturn(Optional.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(Collections.emptyList());

            byte[] result = service.genererGrandLivre(ORG_ID, NUMERO_COMPTE, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("with écritures matching the compte — builds movements and alternates row colors")
        void withEcritures_buildsMouvements() {
            CompteComptable compte = buildCompte(NUMERO_COMPTE, "Clients", 4, TypeCompteComptable.ACTIF);
            CompteComptable autreCompte = buildCompte("512000", "Banque", 5, TypeCompteComptable.TRESORERIE);

            // ligne1 matches the target compte
            LigneEcriture ligne1 = buildLigne(compte, new BigDecimal("10000"), BigDecimal.ZERO);
            // ligne2 is a different compte — should be filtered out
            LigneEcriture ligne2 = buildLigne(autreCompte, BigDecimal.ZERO, new BigDecimal("10000"));

            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Règlement",
                    List.of(ligne1, ligne2));

            // Second ecriture matches too (null lignes — skipped)
            EcritureComptable ecritureNullLignes = buildEcriture(DATE_DEBUT, "ECR-002",
                    "Test null lignes", null);

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisationAndNumero(ORG_ID, NUMERO_COMPTE))
                    .thenReturn(Optional.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture, ecritureNullLignes));

            byte[] result = service.genererGrandLivre(ORG_ID, NUMERO_COMPTE, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("ligne with null compteComptable — skipped")
        void ligneWithNullCompte_skipped() {
            CompteComptable compte = buildCompte(NUMERO_COMPTE, "Clients", 4, TypeCompteComptable.ACTIF);

            LigneEcriture ligneNullCompte = buildLigne(null, new BigDecimal("500"), null);
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Test",
                    List.of(ligneNullCompte));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisationAndNumero(ORG_ID, NUMERO_COMPTE))
                    .thenReturn(Optional.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererGrandLivre(ORG_ID, NUMERO_COMPTE, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("ligne with null montantDebit and null montantCredit — treated as zero")
        void ligneWithNullAmounts_treatedAsZero() {
            CompteComptable compte = buildCompte(NUMERO_COMPTE, "Clients", 4, TypeCompteComptable.ACTIF);

            LigneEcriture ligne = buildLigne(compte, null, null);
            EcritureComptable ecriture = buildEcriture(DATE_DEBUT, "ECR-001", "Test",
                    List.of(ligne));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisationAndNumero(ORG_ID, NUMERO_COMPTE))
                    .thenReturn(Optional.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(ecriture));

            byte[] result = service.genererGrandLivre(ORG_ID, NUMERO_COMPTE, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("multiple matching lignes — cumulative running balance computed correctly")
        void multipleMatchingLignes_cumulativeBalance() {
            CompteComptable compte = buildCompte(NUMERO_COMPTE, "Clients", 4, TypeCompteComptable.ACTIF);

            LigneEcriture l1 = buildLigne(compte, new BigDecimal("1000"), BigDecimal.ZERO);
            LigneEcriture l2 = buildLigne(compte, BigDecimal.ZERO, new BigDecimal("300"));
            LigneEcriture l3 = buildLigne(compte, new BigDecimal("200"), BigDecimal.ZERO);

            EcritureComptable e1 = buildEcriture(DATE_DEBUT, "ECR-001", "Débit", List.of(l1, l2));
            EcritureComptable e2 = buildEcriture(DATE_DEBUT.plusDays(1), "ECR-002", "Autre",
                    List.of(l3));

            when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
            when(compteComptableRepository.findByOrganisationAndNumero(ORG_ID, NUMERO_COMPTE))
                    .thenReturn(Optional.of(compte));
            when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                    .thenReturn(List.of(e1, e2));

            byte[] result = service.genererGrandLivre(ORG_ID, NUMERO_COMPTE, DATE_DEBUT, DATE_FIN);
            assertThat(result).isNotNull().isNotEmpty();
        }
    }

    // ─── addTitrePage — null date range branch ─────────────────────────────────

    @Test
    @DisplayName("genererBalance with null dateDebut and dateFin — no période paragraph rendered")
    void genererBalance_nullDates_noPeriodeParagraph() {
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
        when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(Collections.emptyList());
        when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                .thenReturn(Collections.emptyList());

        // null dateDebut and dateFin should NOT add the période line
        byte[] result = service.genererBalance(ORG_ID, null, null);
        assertThat(result).isNotNull().isNotEmpty();
    }

    @Test
    @DisplayName("genererCompteResultat with null dates — works without période paragraph")
    void genererCompteResultat_nullDates_noPeriodeParagraph() {
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
        when(compteComptableRepository.findByOrganisation(ORG_ID)).thenReturn(Collections.emptyList());
        when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                .thenReturn(Collections.emptyList());

        byte[] result = service.genererCompteResultat(ORG_ID, null, null);
        assertThat(result).isNotNull().isNotEmpty();
    }

    @Test
    @DisplayName("genererGrandLivre with null dates — works without période paragraph")
    void genererGrandLivre_nullDates_noPeriodeParagraph() {
        CompteComptable compte = buildCompte("411000", "Clients", 4, TypeCompteComptable.ACTIF);

        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));
        when(compteComptableRepository.findByOrganisationAndNumero(ORG_ID, "411000"))
                .thenReturn(Optional.of(compte));
        when(ecritureComptableRepository.findByOrganisationAndDateRange(eq(ORG_ID), any(), any()))
                .thenReturn(Collections.emptyList());

        byte[] result = service.genererGrandLivre(ORG_ID, "411000", null, null);
        assertThat(result).isNotNull().isNotEmpty();
    }
}
