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.cotisation.response.CotisationResponse;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.Membre;
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.OrganisationRepository;
import dev.lions.unionflow.server.service.support.SecuriteHelper;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * Tests complémentaires pour {@link CotisationService} — cas limites non couverts
 * par les tests d'intégration principaux.
 */
@QuarkusTest
@DisplayName("CotisationService — branches finales non couvertes")
class CotisationServiceFinalBranchesTest {

    @Inject
    CotisationService cotisationService;

    @InjectMock
    CotisationRepository cotisationRepository;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    OrganisationRepository organisationRepository;

    @InjectMock
    SecuriteHelper securiteHelper;

    @InjectMock
    OrganisationService organisationService;

    // =========================================================================
    // Utilitaires réflexion
    // =========================================================================

    private Object callPrivate(String name, Class<?>[] paramTypes, Object... args) throws Exception {
        Method m = CotisationService.class.getDeclaredMethod(name, paramTypes);
        m.setAccessible(true);
        return m.invoke(cotisationService, args);
    }

    @Test
    @DisplayName("enregistrerPaiement — montantPaye > 0 mais < montantDu → PARTIELLEMENT_PAYEE")
    void enregistrerPaiement_partielPayement_statutPartiellement() {
        UUID cotId = UUID.randomUUID();
        Cotisation cot = new Cotisation();
        cot.setId(cotId);
        cot.setNumeroReference("COT-PARTIEL-001");
        cot.setStatut("EN_ATTENTE");
        cot.setMontantDu(BigDecimal.valueOf(10000));
        cot.setMontantPaye(BigDecimal.ZERO);
        cot.setDateEcheance(LocalDate.now().plusMonths(1));
        cot.setActif(true);

        when(cotisationRepository.findByIdOptional(cotId)).thenReturn(Optional.of(cot));

        CotisationResponse response = cotisationService.enregistrerPaiement(
                cotId, BigDecimal.valueOf(3000), null, null, null);

        assertThat(response).isNotNull();
        assertThat(response.getStatut()).isEqualTo("PARTIELLEMENT_PAYEE");
    }

    @Test
    @DisplayName("enregistrerPaiement — montantPaye == montantDu → PAYEE")
    void enregistrerPaiement_montantEgalDu_statut_PAYEE() {
        UUID cotId = UUID.randomUUID();
        Cotisation cot = new Cotisation();
        cot.setId(cotId);
        cot.setNumeroReference("COT-PAYEE-FULL");
        cot.setStatut("EN_ATTENTE");
        cot.setMontantDu(BigDecimal.valueOf(5000));
        cot.setMontantPaye(BigDecimal.ZERO);
        cot.setDateEcheance(LocalDate.now().plusMonths(1));
        cot.setActif(true);

        when(cotisationRepository.findByIdOptional(cotId)).thenReturn(Optional.of(cot));

        CotisationResponse response = cotisationService.enregistrerPaiement(
                cotId, BigDecimal.valueOf(5000), null, null, null);

        assertThat(response).isNotNull();
        assertThat(response.getStatut()).isEqualTo("PAYEE");
    }

    @Test
    @DisplayName("enregistrerPaiement — montantPaye initial null → statut reste EN_ATTENTE")
    void enregistrerPaiement_cotisationMontantPayeNull_pasDeChangementStatut() {
        UUID cotId = UUID.randomUUID();
        Cotisation cot = new Cotisation();
        cot.setId(cotId);
        cot.setNumeroReference("COT-PAYE-NULL-INIT");
        cot.setStatut("EN_ATTENTE");
        cot.setMontantDu(BigDecimal.valueOf(5000));
        cot.setMontantPaye(null);
        cot.setDateEcheance(LocalDate.now().plusMonths(1));
        cot.setActif(true);

        when(cotisationRepository.findByIdOptional(cotId)).thenReturn(Optional.of(cot));

        CotisationResponse response = cotisationService.enregistrerPaiement(
                cotId, null, null, null, null);

        assertThat(response).isNotNull();
        assertThat(response.getStatut()).isEqualTo("EN_ATTENTE");
    }

    @Test
    @DisplayName("convertToResponse — statut PAYEE non en retard → retardTexte 'Payée'")
    void convertToResponse_statutPayee_nonEnRetard_retardTextePayee() throws Exception {
        Cotisation cot = new Cotisation();
        cot.setId(UUID.randomUUID());
        cot.setNumeroReference("COT-PAYEE-TEXT");
        cot.setTypeCotisation("MENSUELLE");
        cot.setStatut("PAYEE");
        // dateEcheance futur → isEnRetard() = false
        cot.setDateEcheance(LocalDate.now().plusMonths(2));
        cot.setMontantDu(BigDecimal.valueOf(5000));
        cot.setMontantPaye(BigDecimal.valueOf(5000));
        cot.setActif(true);

        Object result = callPrivate("convertToResponse", new Class[]{Cotisation.class}, cot);

        assertThat(result).isNotNull().isInstanceOf(CotisationResponse.class);
        CotisationResponse response = (CotisationResponse) result;
        assertThat(response.getRetardTexte()).isEqualTo("Payée");
        assertThat(response.getRetardCouleur()).isEqualTo("text-green-600");
    }

    @Test
    @DisplayName("convertToResponse — en retard 1 jour → 'jour' sans 's'")
    void convertToResponse_enRetard1Jour_texteJourSansPluriel() throws Exception {
        Cotisation cot = new Cotisation();
        cot.setId(UUID.randomUUID());
        cot.setNumeroReference("COT-RETARD-1JOUR");
        cot.setTypeCotisation("ANNUELLE");
        cot.setStatut("EN_ATTENTE");
        cot.setDateEcheance(LocalDate.now().minusDays(1));
        cot.setMontantDu(BigDecimal.valueOf(8000));
        cot.setMontantPaye(BigDecimal.ZERO);
        cot.setActif(true);

        Object result = callPrivate("convertToResponse", new Class[]{Cotisation.class}, cot);

        assertThat(result).isNotNull().isInstanceOf(CotisationResponse.class);
        CotisationResponse response = (CotisationResponse) result;
        assertThat(response.getRetardCouleur()).isEqualTo("text-red-500");
        assertThat(response.getRetardTexte()).contains("jour").doesNotContain("jours");
    }

    @Test
    @DisplayName("convertToResponse — en retard 5 jours → L427 true + L430 jours > 1 true → 'jours' avec 's'")
    void convertToResponse_enRetard5Jours_texteJoursAvecPluriel() throws Exception {
        Cotisation cot = new Cotisation();
        cot.setId(UUID.randomUUID());
        cot.setNumeroReference("COT-RETARD-5JOURS");
        cot.setTypeCotisation("MENSUELLE");
        cot.setStatut("EN_ATTENTE");
        cot.setDateEcheance(LocalDate.now().minusDays(5));
        cot.setMontantDu(BigDecimal.valueOf(3000));
        cot.setMontantPaye(BigDecimal.ZERO);
        cot.setActif(true);

        Object result = callPrivate("convertToResponse", new Class[]{Cotisation.class}, cot);

        assertThat(result).isNotNull().isInstanceOf(CotisationResponse.class);
        CotisationResponse response = (CotisationResponse) result;
        assertThat(response.getRetardCouleur()).isEqualTo("text-red-500");
        assertThat(response.getRetardTexte()).contains("jours");
    }

    @Test
    @DisplayName("convertToResponse — montantDu == ZERO → pourcentage 0")
    void convertToResponse_montantDuZero_pourcentageZero() throws Exception {
        Cotisation cot = new Cotisation();
        cot.setId(UUID.randomUUID());
        cot.setNumeroReference("COT-DU-ZERO");
        cot.setTypeCotisation("MENSUELLE");
        cot.setStatut("EN_ATTENTE");
        cot.setDateEcheance(LocalDate.now().plusMonths(1));
        cot.setMontantDu(BigDecimal.ZERO);
        cot.setMontantPaye(BigDecimal.ZERO);
        cot.setActif(true);

        Object result = callPrivate("convertToResponse", new Class[]{Cotisation.class}, cot);

        assertThat(result).isNotNull().isInstanceOf(CotisationResponse.class);
        CotisationResponse response = (CotisationResponse) result;
        assertThat(response.getPourcentagePaiement()).isEqualTo(0);
    }

    @Test
    @DisplayName("buildInitiales — prenom null + nom non-null → initiale nom retournée")
    void buildInitiales_prenomNull_nomNonNull_retourneInitialeNom() throws Exception {
        Object result = callPrivate("buildInitiales", new Class[]{String.class, String.class},
                (Object) null, "Diallo");

        assertThat(result).isNotNull();
        assertThat(result.toString()).isEqualTo("D");
    }

    @Test
    @DisplayName("buildInitiales — prenom non-null + nom null → initiale prenom retournée")
    void buildInitiales_prenomNonNull_nomNull_retourneInitialePrenom() throws Exception {
        Object result = callPrivate("buildInitiales", new Class[]{String.class, String.class},
                "Fatou", (Object) null);

        assertThat(result).isNotNull();
        assertThat(result.toString()).isEqualTo("F");
    }

    @Test
    @DisplayName("validateCotisationRules — montantDu positif → pas d'exception")
    void validateCotisationRules_montantDuPositif_pasException() throws Exception {
        Cotisation cot = new Cotisation();
        cot.setMontantDu(BigDecimal.valueOf(1000));
        cot.setDateEcheance(LocalDate.now().plusMonths(1));
        cot.setMontantPaye(BigDecimal.ZERO);
        cot.setStatut("EN_ATTENTE");

        callPrivate("validateCotisationRules", new Class[]{Cotisation.class}, cot);
    }

    @Test
    @DisplayName("validateCotisationRules — montantDu <= 0 → exception levée")
    void validateCotisationRules_montantDuZero_exception() throws Exception {
        Cotisation cot = new Cotisation();
        cot.setMontantDu(BigDecimal.ZERO);

        assertThatThrownBy(() ->
                callPrivate("validateCotisationRules", new Class[]{Cotisation.class}, cot))
                .hasCauseInstanceOf(IllegalArgumentException.class)
                .cause().hasMessageContaining("montant dû doit être positif");
    }

    @Test
    @DisplayName("getMesCotisationsEnAttente — roles={'MEMBRE'} non admin → membre path")
    void getMesCotisationsEnAttente_roleMembre_membrePath_L740False() {
        // roles != null && contains("ADMIN") = false && contains("ADMIN_ORGANISATION") = false
        // → L740 condition false → pas admin path → membre path
        Membre membre = new Membre();
        UUID membreId = UUID.randomUUID();
        membre.setId(membreId);
        membre.setNumeroMembre("UF-L740-TEST");
        membre.setEmail("membre.l740@unionflow.com");
        membre.setActif(true);

        when(securiteHelper.resolveEmail()).thenReturn("membre.l740@unionflow.com");
        when(securiteHelper.getRoles()).thenReturn(Set.of("MEMBRE"));
        when(membreRepository.findByEmail("membre.l740@unionflow.com"))
                .thenReturn(Optional.of(membre));

        jakarta.persistence.EntityManager mockEm = org.mockito.Mockito.mock(jakarta.persistence.EntityManager.class);
        when(cotisationRepository.getEntityManager()).thenReturn(mockEm);

        @SuppressWarnings("unchecked")
        jakarta.persistence.TypedQuery<Cotisation> cotQuery = org.mockito.Mockito.mock(jakarta.persistence.TypedQuery.class);
        when(mockEm.createQuery(org.mockito.ArgumentMatchers.anyString(),
                org.mockito.ArgumentMatchers.eq(Cotisation.class))).thenReturn(cotQuery);
        when(cotQuery.setParameter(org.mockito.ArgumentMatchers.anyString(), any()))
                .thenReturn(cotQuery);
        when(cotQuery.getResultList()).thenReturn(Collections.emptyList());

        List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();

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

    @Test
    @DisplayName("getMesCotisationsEnAttente — roles={'SUPER_ADMIN'} non ADMIN/ADMIN_ORG → membre path avec cotisations")
    void getMesCotisationsEnAttente_roleSuperAdmin_membrePath_avecCotisations() {
        Membre membre = new Membre();
        UUID membreId = UUID.randomUUID();
        membre.setId(membreId);
        membre.setNumeroMembre("UF-SUPER-L740");
        membre.setEmail("superadmin.l740@unionflow.com");
        membre.setActif(true);

        when(securiteHelper.resolveEmail()).thenReturn("superadmin.l740@unionflow.com");
        when(securiteHelper.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));
        when(membreRepository.findByEmail("superadmin.l740@unionflow.com"))
                .thenReturn(Optional.of(membre));

        jakarta.persistence.EntityManager mockEm = org.mockito.Mockito.mock(jakarta.persistence.EntityManager.class);
        when(cotisationRepository.getEntityManager()).thenReturn(mockEm);

        @SuppressWarnings("unchecked")
        jakarta.persistence.TypedQuery<Cotisation> cotQuery = org.mockito.Mockito.mock(jakarta.persistence.TypedQuery.class);
        when(mockEm.createQuery(org.mockito.ArgumentMatchers.anyString(),
                org.mockito.ArgumentMatchers.eq(Cotisation.class))).thenReturn(cotQuery);
        when(cotQuery.setParameter(org.mockito.ArgumentMatchers.anyString(), any()))
                .thenReturn(cotQuery);

        Cotisation cot = new Cotisation();
        cot.setId(UUID.randomUUID());
        cot.setNumeroReference("COT-SUPER-ATTENTE");
        cot.setStatut("EN_ATTENTE");
        cot.setMontantDu(BigDecimal.valueOf(2000));
        cot.setMontantPaye(BigDecimal.ZERO);
        cot.setDateEcheance(LocalDate.now().plusMonths(3));
        cot.setActif(true);
        cot.setMembre(membre);
        when(cotQuery.getResultList()).thenReturn(List.of(cot));

        List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();

        assertThat(result).isNotNull().hasSize(1);
    }

    @Test
    @DisplayName("getTypeMembreLibelle: statutCompte null → retourne chaîne vide (branche null)")
    void getTypeMembreLibelle_null_retourneVideStr() throws Exception {
        java.lang.reflect.Method m = CotisationService.class.getDeclaredMethod("getTypeMembreLibelle", String.class);
        m.setAccessible(true);
        Object result = m.invoke(cotisationService, (String) null);
        assertThat(result).isEqualTo("");
    }

    @Test
    @DisplayName("getTypeMembreLibelle: statutCompte 'ACTIF' → retourne 'Actif' (branche case ACTIF)")
    void getTypeMembreLibelle_actif_retourneActif() throws Exception {
        java.lang.reflect.Method m = CotisationService.class.getDeclaredMethod("getTypeMembreLibelle", String.class);
        m.setAccessible(true);
        Object result = m.invoke(cotisationService, "ACTIF");
        assertThat(result).isEqualTo("Actif");
    }

    @Test
    @DisplayName("convertToResponse — statut null → getTypeMembreLibelle(null) retourne chaîne vide (L529 branche null)")
    void convertToResponse_statutNull_getTypeMembreLibelleNull() throws Exception {
        Cotisation cot = new Cotisation();
        cot.setId(UUID.randomUUID());
        cot.setNumeroReference("COT-STATUT-NULL");
        cot.setTypeCotisation("MENSUELLE");
        cot.setStatut(null);
        cot.setDateEcheance(LocalDate.now().plusMonths(1));
        cot.setMontantDu(BigDecimal.valueOf(3000));
        cot.setMontantPaye(BigDecimal.ZERO);
        cot.setActif(true);

        Object result = callPrivate("convertToResponse", new Class[]{Cotisation.class}, cot);

        assertThat(result).isNotNull().isInstanceOf(CotisationResponse.class);
        CotisationResponse response = (CotisationResponse) result;
        assertThat(response.getStatut()).isNull();
    }
}
