package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import dev.lions.unionflow.server.api.dto.membre.CompteAdherentResponse;
import dev.lions.unionflow.server.api.enums.membre.StatutMembre;

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.CotisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.mutuelle.credit.DemandeCreditRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
import dev.lions.unionflow.server.service.support.SecuriteHelper;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

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

@QuarkusTest
class CompteAdherentServiceTest {

    @Inject
    CompteAdherentService service;

    @InjectMock
    SecuriteHelper securiteHelper;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    CotisationRepository cotisationRepository;

    @InjectMock
    CompteEpargneRepository compteEpargneRepository;

    @InjectMock
    DemandeCreditRepository demandeCreditRepository;

    private final UUID TEST_MEMBRE_ID = UUID.randomUUID();
    private final String TEST_EMAIL = "test@unionflow.test";

    @BeforeEach
    void setup() {
        Mockito.when(securiteHelper.resolveEmail()).thenReturn(TEST_EMAIL);
    }

    @Test
    @DisplayName("getMonCompte sans utilisateur connecté lance NotFoundException")
    void getMonCompte_withoutUser_throws() {
        Mockito.when(securiteHelper.resolveEmail()).thenReturn(null);
        assertThatThrownBy(() -> service.getMonCompte())
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("getMonCompte avec email blank lance NotFoundException (L71 isBlank = true)")
    void getMonCompte_emailBlank_throws() {
        Mockito.when(securiteHelper.resolveEmail()).thenReturn("   "); // non-null mais isBlank = true
        assertThatThrownBy(() -> service.getMonCompte())
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Identité non disponible");
    }

    @Test
    @DisplayName("getMonCompte retourne les données financières agrégées (Mocks)")
    void getMonCompte_withMocks_returnsAggregatedData() {
        // GIVEN
        Membre m = new Membre();
        m.setId(TEST_MEMBRE_ID);
        m.setNom("Lions");
        m.setPrenom("Agent");
        m.setEmail(TEST_EMAIL);
        m.setNumeroMembre("MBR-001");

        Mockito.when(membreRepository.findByEmail(TEST_EMAIL)).thenReturn(Optional.of(m));
        
        // Cotisations: 50.000 payées sur 5 total
        Mockito.when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(TEST_MEMBRE_ID))
                .thenReturn(new BigDecimal("50000"));
        Mockito.when(cotisationRepository.countPayeesByMembreId(TEST_MEMBRE_ID)).thenReturn(5L);
        Mockito.when(cotisationRepository.countByMembreId(TEST_MEMBRE_ID)).thenReturn(5L);

        // Épargne: 100.000 (dont 20.000 bloqué)
        Mockito.when(compteEpargneRepository.sumSoldeActuelByMembreId(TEST_MEMBRE_ID))
                .thenReturn(new BigDecimal("100000"));
        Mockito.when(compteEpargneRepository.sumSoldeBloqueByMembreId(TEST_MEMBRE_ID))
                .thenReturn(new BigDecimal("20000"));
        
        // Crédit: 30.000 encours
        Mockito.when(demandeCreditRepository.calculerTotalEncoursParMembre(TEST_MEMBRE_ID))
                .thenReturn(new BigDecimal("30000"));

        // WHEN
        CompteAdherentResponse response = service.getMonCompte();

        // THEN
        assertThat(response).isNotNull();
        assertThat(response.getNumeroMembre()).isEqualTo("MBR-001");

        // soldeTotalDisponible = 50.000 (cotis) + (100.000 - 20.000) (épargne dispo) = 130.000
        assertThat(response.getSoldeTotalDisponible()).isEqualByComparingTo(new BigDecimal("130000"));

        // capaciteEmprunt = 3 * 80.000 = 240.000
        assertThat(response.getCapaciteEmprunt()).isEqualByComparingTo(new BigDecimal("240000"));

        assertThat(response.getEncoursCreditTotal()).isEqualByComparingTo(new BigDecimal("30000"));
        assertThat(response.getTauxEngagement()).isEqualTo(100);
    }

    @Test
    @DisplayName("getMonCompte couvre lambda$getMonCompte$0 : fallback .or() par email en minuscules")
    void getMonCompte_emailFallback_couvreOrLambda() {
        // GIVEN – email retourné avec majuscules ; lookup direct échoue → fallback .or() déclenché
        String emailMajuscules = "Test@UnionFlow.Test";
        Mockito.when(securiteHelper.resolveEmail()).thenReturn(emailMajuscules);

        Membre m = new Membre();
        m.setId(TEST_MEMBRE_ID);
        m.setNom("Lions");
        m.setPrenom("Or");
        m.setEmail(emailMajuscules.toLowerCase());
        m.setNumeroMembre("MBR-OR-001");
        m.setActif(true);

        // findByEmail(email.trim()) retourne vide → déclenche lambda$0 : () -> findByEmail(toLowerCase)
        Mockito.when(membreRepository.findByEmail(emailMajuscules.trim())).thenReturn(Optional.empty());
        Mockito.when(membreRepository.findByEmail(emailMajuscules.trim().toLowerCase()))
                .thenReturn(Optional.of(m));

        Mockito.when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(TEST_MEMBRE_ID))
                .thenReturn(BigDecimal.ZERO);
        Mockito.when(cotisationRepository.countPayeesByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(cotisationRepository.countByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(compteEpargneRepository.sumSoldeActuelByMembreId(TEST_MEMBRE_ID))
                .thenReturn(BigDecimal.ZERO);
        Mockito.when(compteEpargneRepository.sumSoldeBloqueByMembreId(TEST_MEMBRE_ID))
                .thenReturn(BigDecimal.ZERO);
        Mockito.when(demandeCreditRepository.calculerTotalEncoursParMembre(TEST_MEMBRE_ID))
                .thenReturn(BigDecimal.ZERO);

        // WHEN
        CompteAdherentResponse response = service.getMonCompte();

        // THEN – lambda$0 a été traversé, membre trouvé via fallback
        assertThat(response).isNotNull();
        assertThat(response.getNumeroMembre()).isEqualTo("MBR-OR-001");
    }

    @Test
    @DisplayName("getMonCompte couvre lambda$getMonCompte$2 : NotFoundException quand les deux lookups échouent")
    void getMonCompte_bothLookupsEmpty_throwsNotFound() {
        // Les deux findByEmail retournent Optional.empty() → orElseThrow déclenche lambda$2
        Mockito.when(membreRepository.findByEmail(TEST_EMAIL.trim())).thenReturn(Optional.empty());
        Mockito.when(membreRepository.findByEmail(TEST_EMAIL.trim().toLowerCase()))
                .thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.getMonCompte())
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("getMonCompte retourne données agrégées quand tous les repos retournent null (couvre nvl() v==null)")
    void getMonCompte_reposRetournentNull_nvlUtiliseZero() {
        // GIVEN — tous les repos BigDecimal retournent null → nvl() doit retourner BigDecimal.ZERO
        Membre m = new Membre();
        m.setId(TEST_MEMBRE_ID);
        m.setNom("Null");
        m.setPrenom("Test");
        m.setEmail(TEST_EMAIL);
        m.setNumeroMembre("MBR-NULL-001");
        m.setActif(true);

        Mockito.when(membreRepository.findByEmail(TEST_EMAIL)).thenReturn(Optional.of(m));

        // Retourner null pour tous les BigDecimal → couvre nvl() branche v == null
        Mockito.when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(TEST_MEMBRE_ID))
                .thenReturn(null); // → nvl(null) → BigDecimal.ZERO
        Mockito.when(cotisationRepository.countPayeesByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(cotisationRepository.countByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(cotisationRepository.countRetardByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);

        Mockito.when(compteEpargneRepository.sumSoldeActuelByMembreId(TEST_MEMBRE_ID))
                .thenReturn(null); // → nvl(null) → BigDecimal.ZERO
        Mockito.when(compteEpargneRepository.sumSoldeBloqueByMembreId(TEST_MEMBRE_ID))
                .thenReturn(null); // → nvl(null) → BigDecimal.ZERO
        Mockito.when(compteEpargneRepository.countActifsByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);

        Mockito.when(demandeCreditRepository.calculerTotalEncoursParMembre(TEST_MEMBRE_ID))
                .thenReturn(null); // → nvl(null) → BigDecimal.ZERO

        // WHEN
        CompteAdherentResponse response = service.getMonCompte();

        // THEN — nvl() a retourné ZERO pour toutes les valeurs null
        assertThat(response).isNotNull();
        assertThat(response.getSoldeCotisations()).isEqualByComparingTo(java.math.BigDecimal.ZERO);
        assertThat(response.getSoldeEpargne()).isEqualByComparingTo(java.math.BigDecimal.ZERO);
        assertThat(response.getSoldeBloque()).isEqualByComparingTo(java.math.BigDecimal.ZERO);
        assertThat(response.getEncoursCreditTotal()).isEqualByComparingTo(java.math.BigDecimal.ZERO);
        assertThat(response.getSoldeTotalDisponible()).isEqualByComparingTo(java.math.BigDecimal.ZERO);
        assertThat(response.getCapaciteEmprunt()).isEqualByComparingTo(java.math.BigDecimal.ZERO);
    }

    @Test
    @DisplayName("getMonCompte avec membre inactif (actif=false) lance NotFoundException")
    void getMonCompte_membreInactif_throwsNotFound() {
        // GIVEN — membre trouvé mais actif == false → filter(m -> m.getActif() == null || m.getActif()) échoue
        Membre membreInactif = new Membre();
        membreInactif.setId(TEST_MEMBRE_ID);
        membreInactif.setNom("Inactif");
        membreInactif.setPrenom("Test");
        membreInactif.setEmail(TEST_EMAIL);
        membreInactif.setActif(false); // inactif

        Mockito.when(membreRepository.findByEmail(TEST_EMAIL)).thenReturn(Optional.of(membreInactif));
        // Le fallback lowercase retourne aussi vide (membre inactif filtré)
        Mockito.when(membreRepository.findByEmail(TEST_EMAIL.toLowerCase()))
                .thenReturn(Optional.of(membreInactif));

        // WHEN / THEN — le filter(actif) rejette le membre → orElseThrow
        assertThatThrownBy(() -> service.getMonCompte())
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("getMonCompte avec tauxEngagement: nbTotal == 0 → tauxEngagement null")
    void getMonCompte_nbTotalZero_tauxEngagementNull() {
        // GIVEN — nbTotal = 0 → if (nbTotal > 0) est faux → tauxEngagement reste null
        Membre m = new Membre();
        m.setId(TEST_MEMBRE_ID);
        m.setNom("Zero");
        m.setPrenom("Total");
        m.setEmail(TEST_EMAIL);
        m.setNumeroMembre("MBR-ZERO-001");

        Mockito.when(membreRepository.findByEmail(TEST_EMAIL)).thenReturn(Optional.of(m));

        Mockito.when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(TEST_MEMBRE_ID))
                .thenReturn(java.math.BigDecimal.ZERO);
        Mockito.when(cotisationRepository.countPayeesByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(cotisationRepository.countByMembreId(TEST_MEMBRE_ID)).thenReturn(0L); // nbTotal = 0
        Mockito.when(cotisationRepository.countRetardByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(compteEpargneRepository.sumSoldeActuelByMembreId(TEST_MEMBRE_ID))
                .thenReturn(java.math.BigDecimal.ZERO);
        Mockito.when(compteEpargneRepository.sumSoldeBloqueByMembreId(TEST_MEMBRE_ID))
                .thenReturn(java.math.BigDecimal.ZERO);
        Mockito.when(compteEpargneRepository.countActifsByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(demandeCreditRepository.calculerTotalEncoursParMembre(TEST_MEMBRE_ID))
                .thenReturn(java.math.BigDecimal.ZERO);

        // WHEN
        CompteAdherentResponse response = service.getMonCompte();

        // THEN — nbTotal == 0 → tauxEngagement == null (branche if else non exécutée)
        assertThat(response).isNotNull();
        assertThat(response.getTauxEngagement()).isNull();
    }

    @Test
    @DisplayName("buildCompteAdherent couvre lambdas $3/$4/$5 : filter(mo !=null+org), filter(isActif), map(org.nom)")
    void buildCompteAdherent_avecOrganisationActive_couvreFilterEtMapLambdas() {
        // GIVEN – membre avec un MembreOrganisation actif lié à une Organisation
        Membre m = new Membre();
        m.setId(TEST_MEMBRE_ID);
        m.setNom("Lions");
        m.setPrenom("Org");
        m.setEmail(TEST_EMAIL);
        m.setNumeroMembre("MBR-ORG-001");
        m.setActif(true);

        Organisation org = new Organisation();
        org.setNom("Organisation Test Coverage");

        MembreOrganisation mo = new MembreOrganisation();
        mo.setOrganisation(org);
        mo.setStatutMembre(StatutMembre.ACTIF);   // isActif() → true si StatutMembre.ACTIF
        mo.setActif(true);                         // BaseEntity.actif doit être true
        mo.setDateAdhesion(LocalDate.of(2024, 1, 1));

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

        Mockito.when(membreRepository.findByEmail(TEST_EMAIL)).thenReturn(Optional.of(m));

        Mockito.when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(TEST_MEMBRE_ID))
                .thenReturn(BigDecimal.ZERO);
        Mockito.when(cotisationRepository.countPayeesByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(cotisationRepository.countByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(compteEpargneRepository.sumSoldeActuelByMembreId(TEST_MEMBRE_ID))
                .thenReturn(BigDecimal.ZERO);
        Mockito.when(compteEpargneRepository.sumSoldeBloqueByMembreId(TEST_MEMBRE_ID))
                .thenReturn(BigDecimal.ZERO);
        Mockito.when(demandeCreditRepository.calculerTotalEncoursParMembre(TEST_MEMBRE_ID))
                .thenReturn(BigDecimal.ZERO);

        // WHEN
        CompteAdherentResponse response = service.getMonCompte();

        // THEN – lambdas $3 (filter non-null), $4 (filter isActif), $5 (map nom) tous traversés
        assertThat(response).isNotNull();
        assertThat(response.getOrganisationNom()).isEqualTo("Organisation Test Coverage");
    }

    @Test
    @DisplayName("buildCompteAdherent avec 2 MembreOrganisation actifs — lambda$4 Comparator.comparing invoqué")
    void buildCompteAdherent_avecDeuxOrganisationsActives_couvreComparateurLambda() {
        // Pour invoquer lambda$buildCompteAdherent$4 (Comparator.comparing(mo -> mo.getDateAdhesion() != null ? ...))
        // il faut AU MOINS 2 éléments dans le stream pour que max() appelle le comparateur

        Membre m = new Membre();
        m.setId(TEST_MEMBRE_ID);
        m.setNom("Deux");
        m.setPrenom("Orgs");
        m.setEmail(TEST_EMAIL);
        m.setNumeroMembre("MBR-2ORG");
        m.setActif(true);

        Organisation org1 = new Organisation();
        org1.setNom("Org Ancienne");

        Organisation org2 = new Organisation();
        org2.setNom("Org Récente");

        // mo1 : dateAdhesion non-null (branche true du ternaire)
        MembreOrganisation mo1 = new MembreOrganisation();
        mo1.setOrganisation(org1);
        mo1.setStatutMembre(StatutMembre.ACTIF);
        mo1.setActif(true);
        mo1.setDateAdhesion(LocalDate.of(2020, 1, 1));

        // mo2 : dateAdhesion non-null, plus récente → sélectionnée par max()
        MembreOrganisation mo2 = new MembreOrganisation();
        mo2.setOrganisation(org2);
        mo2.setStatutMembre(StatutMembre.ACTIF);
        mo2.setActif(true);
        mo2.setDateAdhesion(LocalDate.of(2023, 6, 15));

        m.setMembresOrganisations(List.of(mo1, mo2));

        Mockito.when(membreRepository.findByEmail(TEST_EMAIL)).thenReturn(Optional.of(m));
        Mockito.when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(TEST_MEMBRE_ID)).thenReturn(BigDecimal.ZERO);
        Mockito.when(cotisationRepository.countPayeesByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(cotisationRepository.countByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(compteEpargneRepository.sumSoldeActuelByMembreId(TEST_MEMBRE_ID)).thenReturn(BigDecimal.ZERO);
        Mockito.when(compteEpargneRepository.sumSoldeBloqueByMembreId(TEST_MEMBRE_ID)).thenReturn(BigDecimal.ZERO);
        Mockito.when(demandeCreditRepository.calculerTotalEncoursParMembre(TEST_MEMBRE_ID)).thenReturn(BigDecimal.ZERO);

        CompteAdherentResponse response = service.getMonCompte();

        // L'organisation la plus récente (2023) doit être sélectionnée
        assertThat(response.getOrganisationNom()).isEqualTo("Org Récente");
    }

    @Test
    @DisplayName("buildCompteAdherent avec mo ayant dateAdhesion=null — branche false du ternaire couverte")
    void buildCompteAdherent_avecDateAdhesionNull_couvreTerminaireNull() {
        // Couvre la branche: mo.getDateAdhesion() == null → retourne LocalDate.MIN

        Membre m = new Membre();
        m.setId(TEST_MEMBRE_ID);
        m.setNom("NullDate");
        m.setPrenom("Test");
        m.setEmail(TEST_EMAIL);
        m.setNumeroMembre("MBR-NDATE");
        m.setActif(true);

        Organisation org1 = new Organisation();
        org1.setNom("Org Sans Date");

        Organisation org2 = new Organisation();
        org2.setNom("Org Avec Date");

        // mo1 : dateAdhesion null → branche false (LocalDate.MIN)
        MembreOrganisation mo1 = new MembreOrganisation();
        mo1.setOrganisation(org1);
        mo1.setStatutMembre(StatutMembre.ACTIF);
        mo1.setActif(true);
        mo1.setDateAdhesion(null);

        // mo2 : dateAdhesion non-null → branche true
        MembreOrganisation mo2 = new MembreOrganisation();
        mo2.setOrganisation(org2);
        mo2.setStatutMembre(StatutMembre.ACTIF);
        mo2.setActif(true);
        mo2.setDateAdhesion(LocalDate.of(2024, 3, 1));

        m.setMembresOrganisations(List.of(mo1, mo2));

        Mockito.when(membreRepository.findByEmail(TEST_EMAIL)).thenReturn(Optional.of(m));
        Mockito.when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(TEST_MEMBRE_ID)).thenReturn(BigDecimal.ZERO);
        Mockito.when(cotisationRepository.countPayeesByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(cotisationRepository.countByMembreId(TEST_MEMBRE_ID)).thenReturn(0L);
        Mockito.when(compteEpargneRepository.sumSoldeActuelByMembreId(TEST_MEMBRE_ID)).thenReturn(BigDecimal.ZERO);
        Mockito.when(compteEpargneRepository.sumSoldeBloqueByMembreId(TEST_MEMBRE_ID)).thenReturn(BigDecimal.ZERO);
        Mockito.when(demandeCreditRepository.calculerTotalEncoursParMembre(TEST_MEMBRE_ID)).thenReturn(BigDecimal.ZERO);

        CompteAdherentResponse response = service.getMonCompte();

        // mo2 avec 2024 > LocalDate.MIN → org2 sélectionnée
        assertThat(response.getOrganisationNom()).isEqualTo("Org Avec Date");
    }
}

