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.api.dto.analytics.KPITrendResponse;
import dev.lions.unionflow.server.api.dto.comptabilite.response.EcritureComptableResponse;
import dev.lions.unionflow.server.api.dto.comptabilite.response.LigneEcritureResponse;
import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneRequest;
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse;
import dev.lions.unionflow.server.api.dto.solidarite.request.CreateDemandeAideRequest;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.StatutCompteEpargne;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeTransactionEpargne;
import dev.lions.unionflow.server.entity.Configuration;
import dev.lions.unionflow.server.entity.EcritureComptable;
import dev.lions.unionflow.server.entity.LigneEcriture;
import dev.lions.unionflow.server.entity.Paiement;
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
import dev.lions.unionflow.server.entity.mutuelle.epargne.TransactionEpargne;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.mapper.mutuelle.epargne.TransactionEpargneMapper;
import dev.lions.unionflow.server.repository.ConfigurationRepository;
import dev.lions.unionflow.server.repository.ParametresLcbFtRepository;
import dev.lions.unionflow.server.repository.PaiementRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.TransactionEpargneRepository;
import dev.lions.unionflow.server.service.mutuelle.epargne.TransactionEpargneService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.ws.rs.NotFoundException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * Tests ciblés pour couvrir les branches manquantes restantes dans plusieurs services.
 */
@QuarkusTest
class FinalBranchCoverageTest {

    // =========================================================================
    // ConfigurationService.obtenirConfiguration L45 — config présente mais actif=false
    // =========================================================================

    @Inject
    ConfigurationService configurationService;

    @InjectMock
    ConfigurationRepository configurationRepository;

    @Test
    @DisplayName("ConfigurationService.obtenirConfiguration — config trouvée mais inactif → NotFoundException")
    void obtenirConfiguration_configInactive_throwsNotFound() {
        Configuration config = new Configuration();
        config.setCle("app.feature");
        config.setActif(false);

        when(configurationRepository.findByCle("app.feature"))
                .thenReturn(Optional.of(config));

        assertThatThrownBy(() -> configurationService.obtenirConfiguration("app.feature"))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("ConfigurationService.obtenirConfiguration — config absente → NotFoundException")
    void obtenirConfiguration_configAbsente_throwsNotFound() {
        when(configurationRepository.findByCle("unknown.key"))
                .thenReturn(Optional.empty());

        assertThatThrownBy(() -> configurationService.obtenirConfiguration("unknown.key"))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("ConfigurationService.obtenirConfiguration — config trouvée et active → retourne DTO")
    void obtenirConfiguration_configActive_returnsDto() {
        Configuration config = new Configuration();
        config.setId(UUID.randomUUID());
        config.setCle("app.version");
        config.setValeur("1.0.0");
        config.setActif(true);

        when(configurationRepository.findByCle("app.version"))
                .thenReturn(Optional.of(config));

        var response = configurationService.obtenirConfiguration("app.version");

        assertThat(response).isNotNull();
        assertThat(response.getCle()).isEqualTo("app.version");
    }

    // =========================================================================
    // DemandeAideService.creerDemande L72 — membreDemandeurId non-null mais membre introuvable
    // DemandeAideService.creerDemande L79 — associationId non-null mais org introuvable
    //
    // Stratégie : vrais repositories + UUIDs aléatoires inexistants en base.
    // BaseRepository.findById(UUID) appelle entityManager.find() directement et
    // échappe au mock Mockito via Quarkus Panache — on utilise les vrais repos.
    // =========================================================================

    @Inject
    EntityManager entityManager;

    @Inject
    DemandeAideService demandeAideService;

    @Test
    @TestTransaction
    @DisplayName("DemandeAideService.creerDemande — membreDemandeurId non-null mais membre introuvable → exception (L72 true branch)")
    void creerDemande_membreDemandeurNonNull_membreIntrouvable_throwsException() {
        UUID membreId = UUID.randomUUID();

        CreateDemandeAideRequest request = CreateDemandeAideRequest.builder()
                .typeAide(TypeAide.AIDE_FINANCIERE_URGENTE)
                .titre("Demande urgente")
                .description("Description")
                .membreDemandeurId(membreId)
                .associationId(UUID.randomUUID())
                .build();

        assertThatThrownBy(() -> demandeAideService.creerDemande(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("Membre demandeur non trouvé");
    }

    @Test
    @TestTransaction
    @DisplayName("DemandeAideService.creerDemande — associationId non-null mais org introuvable → exception (L79 true branch)")
    void creerDemande_associationIdNonNull_orgIntrouvable_throwsException() {
        Membre membre = Membre.builder()
                .numeroMembre("UF-L79-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase())
                .prenom("Test").nom("L79")
                .email("l79." + UUID.randomUUID() + "@test.com")
                .dateNaissance(LocalDate.of(1990, 1, 1))
                .build();
        membre.setDateCreation(LocalDateTime.now());
        membre.setActif(true);
        membre.setStatutCompte("ACTIF");
        entityManager.persist(membre);
        entityManager.flush();

        UUID orgId = UUID.randomUUID();

        CreateDemandeAideRequest request = CreateDemandeAideRequest.builder()
                .typeAide(TypeAide.DON_MATERIEL)
                .titre("Demande org")
                .description("Description")
                .membreDemandeurId(membre.getId())
                .associationId(orgId)
                .build();

        assertThatThrownBy(() -> demandeAideService.creerDemande(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("Organisation non trouvée");
    }

    // =========================================================================
    // PaiementService.enrichirLibelles L608 — methodePaiement non-null
    // =========================================================================

    @Inject
    PaiementService paiementService;

    @InjectMock
    PaiementRepository paiementRepository;

    @InjectMock
    TypeReferenceRepository typeReferenceRepository;

    @Test
    @DisplayName("PaiementService.enrichirLibelles — methodePaiement non-null → libellé résolu (branche L605 true)")
    void enrichirLibelles_methodePaiementNonNull_libelleResolu() {
        UUID paiementId = UUID.randomUUID();

        Paiement paiement = new Paiement();
        paiement.setId(paiementId);
        paiement.setNumeroReference("PAY-605-TRUE");
        paiement.setMontant(BigDecimal.TEN);
        paiement.setCodeDevise("XOF");
        paiement.setMethodePaiement("WAVE");
        paiement.setStatutPaiement(null);
        paiement.setActif(true);

        when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement));
        when(typeReferenceRepository.findByDomaineAndCode("METHODE_PAIEMENT", "WAVE"))
                .thenReturn(Optional.empty());

        PaiementResponse response = paiementService.trouverParId(paiementId);

        assertThat(response).isNotNull();
        assertThat(response.getMethodePaiementLibelle()).isEqualTo("WAVE");
    }

    // =========================================================================
    // TransactionEpargneService.validerLcbFtSiSeuilAtteint L229 — montant null → early return
    // L128 — montant < seuil → audit non appelé
    // =========================================================================

    @Inject
    TransactionEpargneService transactionEpargneService;

    @InjectMock
    TransactionEpargneRepository transactionEpargneRepository;

    @InjectMock
    CompteEpargneRepository compteEpargneRepository;

    @InjectMock
    TransactionEpargneMapper transactionEpargneMapper;

    @InjectMock
    ParametresLcbFtRepository parametresLcbFtRepository;

    @InjectMock
    AuditService auditService;

    @InjectMock
    AlerteLcbFtService alerteLcbFtService;

    @BeforeEach
    void setupTransactionMocks() {
        when(parametresLcbFtRepository.getSeuilJustification(any(), any()))
                .thenReturn(Optional.of(new BigDecimal("500000")));
    }

    @Test
    @DisplayName("TransactionEpargneService — montant null → validerLcbFtSiSeuilAtteint early return (L229 true branch)")
    void executerTransaction_montantNull_validerLcbFtEarlyReturn() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = new CompteEpargne();
        compte.setId(compteId);
        compte.setStatut(StatutCompteEpargne.ACTIF);
        compte.setSoldeActuel(new BigDecimal("1000"));
        compte.setSoldeBloque(BigDecimal.ZERO);

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(null)
                .build();

        TransactionEpargne entity = new TransactionEpargne();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(transactionEpargneMapper.toEntity(request)).thenReturn(entity);
        when(transactionEpargneMapper.toDto(entity)).thenReturn(null);

        assertThatThrownBy(() -> transactionEpargneService.executerTransaction(request))
                .isInstanceOf(NullPointerException.class);
    }

    @Test
    @DisplayName("TransactionEpargneService L128 — montant < seuil → condition false → audit non appelé")
    void executerTransaction_L128_montantBelowSeuil_noAudit() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = new CompteEpargne();
        compte.setId(compteId);
        compte.setStatut(StatutCompteEpargne.ACTIF);
        compte.setSoldeActuel(new BigDecimal("2000000"));
        compte.setSoldeBloque(BigDecimal.ZERO);

        TransactionEpargne entity = new TransactionEpargne();

        TransactionEpargneRequest request = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("100"))
                .build();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(transactionEpargneMapper.toEntity(request)).thenReturn(entity);
        when(transactionEpargneMapper.toDto(entity)).thenReturn(null);

        transactionEpargneService.executerTransaction(request);

        org.mockito.Mockito.verify(auditService,
                org.mockito.Mockito.never()).logLcbFtSeuilAtteint(any(), any(), any(), any(), any(), any());
    }

    // =========================================================================
    // TrendAnalysisService.calculerTendanceLineaire L223 — denominateurR == 0
    // =========================================================================

    @Inject
    TrendAnalysisService trendAnalysisService;

    @InjectMock
    KPICalculatorService kpiCalculatorService;

    @Test
    @DisplayName("TrendAnalysisService — toutes les valeurs égales → denominateurR=0, coefficientCorrelation=0 (branche false L223)")
    void calculerTendance_toutesValeursEgales_denominateurRZero() {
        UUID organisationId = UUID.randomUUID();

        when(kpiCalculatorService.calculerTousLesKPI(any(), any(), any()))
                .thenReturn(Map.of(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("100")));

        KPITrendResponse response = trendAnalysisService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS,
                PeriodeAnalyse.CETTE_SEMAINE,
                organisationId);

        assertThat(response).isNotNull();
        assertThat(response.getCoefficientCorrelation()).isNotNull();
    }

    // =========================================================================
    // ComptabiliteService — branches privées via réflexion
    // L360: ecriture.getLignes() == null (false branche)
    // L414: dto.lignes() == null (false branche)
    // L439: ligne.getEcriture() == null (false branche)
    // =========================================================================

    @Inject
    ComptabiliteService comptabiliteService;

    @Test
    @DisplayName("ComptabiliteService.convertToResponse(EcritureComptable) — lignes null → pas de setLignes (L360 false)")
    void convertToResponse_ecriture_lignesNull_skipsLignes() throws Exception {
        ComptabiliteService actualService =
                (ComptabiliteService) ((io.quarkus.arc.ClientProxy) comptabiliteService).arc_contextualInstance();
        Method method = ComptabiliteService.class.getDeclaredMethod(
                "convertToResponse",
                EcritureComptable.class);
        method.setAccessible(true);

        EcritureComptable ecriture = new EcritureComptable();
        ecriture.setLibelle("Test sans lignes");

        // Force lignes=null via réflexion pour couvrir la branche false (null) à L360
        java.lang.reflect.Field lignesField = EcritureComptable.class.getDeclaredField("lignes");
        lignesField.setAccessible(true);
        lignesField.set(ecriture, null);

        Object result = method.invoke(actualService, ecriture);
        assertThat(result).isNotNull();

        var response = (EcritureComptableResponse) result;
        assertThat(response.getLignes()).isNullOrEmpty();
    }

    @Test
    @DisplayName("EcritureComptable.onCreate() — lignes null → branche lignes!=null false (L163)")
    void ecritureComptable_onCreate_lignesNull_skipsCalculerTotaux() throws Exception {
        EcritureComptable ecriture = new EcritureComptable();
        ecriture.setLibelle("Test L163");
        ecriture.setDateEcriture(LocalDate.now());

        // Force lignes=null pour couvrir la branche A=false de "lignes != null && !lignes.isEmpty()" à L163
        java.lang.reflect.Field lignesField = EcritureComptable.class.getDeclaredField("lignes");
        lignesField.setAccessible(true);
        lignesField.set(ecriture, null);

        // Appel direct de @PrePersist via réflexion
        java.lang.reflect.Method onCreateMethod = EcritureComptable.class.getDeclaredMethod("onCreate");
        onCreateMethod.setAccessible(true);
        onCreateMethod.invoke(ecriture);

        // La méthode doit s'exécuter sans exception
        assertThat(ecriture.getLibelle()).isEqualTo("Test L163");
    }

    @Test
    @DisplayName("TransactionEpargneService L128 — getMontant() retourne null au 4ème appel → A=false branche couverte")
    void executerTransaction_L128_getMontantNullOnFinalCheck_falseBranchCovered() {
        UUID compteId = UUID.randomUUID();
        CompteEpargne compte = new CompteEpargne();
        compte.setId(compteId);
        compte.setStatut(StatutCompteEpargne.ACTIF);
        compte.setSoldeActuel(new BigDecimal("1000"));
        compte.setSoldeBloque(BigDecimal.ZERO);

        TransactionEpargne entity = new TransactionEpargne();

        // Séquence getMontant() :
        // Appel 1 (L229) : 10 → non-null → ne pas retourner early
        // Appel 2 (L232) : 10 → 10 < 500000 → pas d'erreur LCB-FT
        // Appel 3 (L80)  : 10 → montant=10 pour les calculs de solde
        // Appel 4 (L128) : null → null != null → false → branche A=false couverte!
        TransactionEpargneRequest realRequest = TransactionEpargneRequest.builder()
                .compteId(compteId.toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(new BigDecimal("10"))
                .build();
        TransactionEpargneRequest spyRequest = org.mockito.Mockito.spy(realRequest);
        org.mockito.Mockito.doReturn(new BigDecimal("10"),
                new BigDecimal("10"),
                new BigDecimal("10"),
                (BigDecimal) null)
                .when(spyRequest).getMontant();

        when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
        when(transactionEpargneMapper.toEntity(any())).thenReturn(entity);
        when(transactionEpargneMapper.toDto(entity)).thenReturn(null);

        transactionEpargneService.executerTransaction(spyRequest);

        org.mockito.Mockito.verify(auditService, org.mockito.Mockito.never())
                .logLcbFtSeuilAtteint(any(), any(), any(), any(), any(), any());
    }

    @Test
    @DisplayName("ComptabiliteService.convertToResponse(LigneEcriture) — ecriture null → ecritureId null (L439 false)")
    void convertToResponse_ligne_ecritureNull_ecritureIdNull() throws Exception {
        ComptabiliteService actualService =
                (ComptabiliteService) ((io.quarkus.arc.ClientProxy) comptabiliteService).arc_contextualInstance();
        Method method = ComptabiliteService.class.getDeclaredMethod(
                "convertToResponse",
                LigneEcriture.class);
        method.setAccessible(true);

        LigneEcriture ligne = new LigneEcriture();
        ligne.setLibelle("Ligne sans écriture");
        ligne.setMontantDebit(BigDecimal.TEN);
        ligne.setMontantCredit(BigDecimal.ZERO);

        Object result = method.invoke(actualService, ligne);
        assertThat(result).isNotNull();

        var response = (LigneEcritureResponse) result;
        assertThat(response.getEcritureId()).isNull();
    }

    @Test
    @DisplayName("ComptabiliteService.convertToEntity(CreateEcritureComptableRequest) — lignes null → pas d'ajout lignes (L414 false)")
    void convertToEntity_ecriture_lignesNull_skipsLignesAdd() throws Exception {
        ComptabiliteService actualService =
                (ComptabiliteService) ((io.quarkus.arc.ClientProxy) comptabiliteService).arc_contextualInstance();
        Method method = ComptabiliteService.class.getDeclaredMethod(
                "convertToEntity",
                dev.lions.unionflow.server.api.dto.comptabilite.request.CreateEcritureComptableRequest.class);
        method.setAccessible(true);

        var dto = dev.lions.unionflow.server.api.dto.comptabilite.request.CreateEcritureComptableRequest.builder()
                .numeroPiece("PIECE-NULL-LIGNES")
                .dateEcriture(java.time.LocalDate.now())
                .libelle("Écriture sans lignes")
                .montantDebit(new BigDecimal("500"))
                .montantCredit(new BigDecimal("500"))
                .lignes(null)
                .build();

        Object result = method.invoke(actualService, dto);
        assertThat(result).isNotNull();

        EcritureComptable ecriture = (EcritureComptable) result;
        assertThat(ecriture.getLignes()).isEmpty();
    }
}
