package dev.lions.unionflow.server.service;

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

import dev.lions.unionflow.server.api.dto.analytics.KPITrendResponse;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import java.math.BigDecimal;
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;

@QuarkusTest
class TrendAnalysisServiceTest {

    @Inject
    TrendAnalysisService trendService;

    @InjectMock
    KPICalculatorService kpiCalculatorService;

    @InjectMock
    OrganisationRepository organisationRepository;

    @BeforeEach
    void setupMocks() {
        // Par défaut, retourner ZERO pour tous les KPI
        when(kpiCalculatorService.calculerTousLesKPI(any(), any(), any()))
                .thenReturn(Map.of(
                        TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("50"),
                        TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, new BigDecimal("100000"),
                        TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, new BigDecimal("5"),
                        TypeMetrique.NOMBRE_DEMANDES_AIDE, new BigDecimal("10")));

        // Par défaut organisationRepository retourne empty
        when(organisationRepository.findByIdOptional(any())).thenReturn(Optional.empty());
    }

    // =========================================================================
    // calculerTendance — TypeMetrique.NOMBRE_MEMBRES_ACTIFS
    // =========================================================================

    @Test
    @DisplayName("calculerTendance NOMBRE_MEMBRES_ACTIFS génère des statistiques et prédictions")
    void calculerTendance_nombreMembresActifs_generatesStats() {
        UUID organisationId = UUID.randomUUID();

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

        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.CE_MOIS, organisationId);

        assertThat(response).isNotNull();
        assertThat(response.getPointsDonnees()).isNotEmpty();
        assertThat(response.getValeurMoyenne()).isNotNull();
        assertThat(response.getTendanceGenerale()).isNotNull();
        assertThat(response.getPredictionProchainePeriode()).isNotNull();
        assertThat(response.getTypeMetrique()).isEqualTo(TypeMetrique.NOMBRE_MEMBRES_ACTIFS);
        assertThat(response.getPeriodeAnalyse()).isEqualTo(PeriodeAnalyse.CE_MOIS);
    }

    // =========================================================================
    // calculerTendance — TypeMetrique.TOTAL_COTISATIONS_COLLECTEES
    // =========================================================================

    @Test
    @DisplayName("calculerTendance TOTAL_COTISATIONS_COLLECTEES retourne une réponse valide")
    void calculerTendance_totalCotisations_returnsValidResponse() {
        UUID organisationId = UUID.randomUUID();

        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, PeriodeAnalyse.CETTE_SEMAINE, organisationId);

        assertThat(response).isNotNull();
        assertThat(response.getTypeMetrique()).isEqualTo(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES);
        assertThat(response.getValeurActuelle()).isNotNull();
    }

    // =========================================================================
    // calculerTendance — TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES
    // =========================================================================

    @Test
    @DisplayName("calculerTendance NOMBRE_EVENEMENTS_ORGANISES retourne une réponse valide")
    void calculerTendance_nombreEvenements_returnsValidResponse() {
        UUID organisationId = UUID.randomUUID();

        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, PeriodeAnalyse.TROIS_DERNIERS_MOIS, organisationId);

        assertThat(response).isNotNull();
        assertThat(response.getTypeMetrique()).isEqualTo(TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES);
    }

    // =========================================================================
    // calculerTendance — TypeMetrique.NOMBRE_DEMANDES_AIDE
    // =========================================================================

    @Test
    @DisplayName("calculerTendance NOMBRE_DEMANDES_AIDE retourne une réponse valide")
    void calculerTendance_nombreDemandesAide_returnsValidResponse() {
        UUID organisationId = UUID.randomUUID();

        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_DEMANDES_AIDE, PeriodeAnalyse.SIX_DERNIERS_MOIS, organisationId);

        assertThat(response).isNotNull();
        assertThat(response.getTypeMetrique()).isEqualTo(TypeMetrique.NOMBRE_DEMANDES_AIDE);
    }

    // =========================================================================
    // calculerTendance — TypeMetrique par défaut (non géré par switch)
    // =========================================================================

    @Test
    @DisplayName("calculerTendance avec TypeMetrique non géré retourne ZERO pour les valeurs")
    void calculerTendance_defaultTypeMetrique_returnsZeroValues() {
        UUID organisationId = UUID.randomUUID();

        // TAUX_CROISSANCE_MEMBRES n'est pas géré explicitement → default → BigDecimal.ZERO
        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.TAUX_CROISSANCE_MEMBRES, PeriodeAnalyse.SEPT_DERNIERS_JOURS, organisationId);

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

    // =========================================================================
    // calculerTendance — avec organisationId null
    // =========================================================================

    @Test
    @DisplayName("calculerTendance avec organisationId null retourne une réponse valide")
    void calculerTendance_nullOrganisationId_returnsValidResponse() {
        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.CETTE_ANNEE, null);

        assertThat(response).isNotNull();
        assertThat(response.getOrganisationId()).isNull();
        assertThat(response.getNomOrganisation()).isNull();
    }

    // =========================================================================
    // calculerTendance — différentes périodes (branches determinerFrequenceMiseAJour)
    // =========================================================================

    @Test
    @DisplayName("calculerTendance pour AUJOURD_HUI retourne fréquence de mise à jour 15 min")
    void calculerTendance_aujourdhui_returns15minFrequency() {
        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.AUJOURD_HUI, null);

        assertThat(response).isNotNull();
        assertThat(response.getFrequenceMiseAJourMinutes()).isEqualTo(15);
    }

    @Test
    @DisplayName("calculerTendance pour CETTE_SEMAINE retourne fréquence de mise à jour 60 min")
    void calculerTendance_cetteSemaine_returns60minFrequency() {
        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.CETTE_SEMAINE, null);

        assertThat(response).isNotNull();
        assertThat(response.getFrequenceMiseAJourMinutes()).isEqualTo(60);
    }

    @Test
    @DisplayName("calculerTendance pour CE_MOIS retourne fréquence de mise à jour 240 min")
    void calculerTendance_ceMois_returns240minFrequency() {
        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.CE_MOIS, null);

        assertThat(response).isNotNull();
        assertThat(response.getFrequenceMiseAJourMinutes()).isEqualTo(240);
    }

    @Test
    @DisplayName("calculerTendance pour CETTE_ANNEE retourne fréquence de mise à jour 1440 min")
    void calculerTendance_cetteAnnee_returns1440minFrequency() {
        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.CETTE_ANNEE, null);

        assertThat(response).isNotNull();
        assertThat(response.getFrequenceMiseAJourMinutes()).isEqualTo(1440);
    }

    // =========================================================================
    // Vérification des champs de tendance et alertes
    // =========================================================================

    @Test
    @DisplayName("calculerTendance retourne les seuils d'alerte et le coefficient de corrélation")
    void calculerTendance_returnsAlertsAndCorrelation() {
        UUID organisationId = UUID.randomUUID();

        // Fournir des valeurs variées pour forcer le calcul des stats réelles
        when(kpiCalculatorService.calculerTousLesKPI(eq(organisationId), any(), any()))
                .thenReturn(Map.of(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("80")))
                .thenReturn(Map.of(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("100")))
                .thenReturn(Map.of(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("200")));

        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.TROIS_DERNIERS_MOIS, organisationId);

        assertThat(response).isNotNull();
        assertThat(response.getCoefficientCorrelation()).isNotNull();
        assertThat(response.getSeuilAlerteBas()).isNotNull();
        assertThat(response.getSeuilAlerteHaut()).isNotNull();
        assertThat(response.getAlerteActive()).isNotNull();
        assertThat(response.getMargeErreurPrediction()).isNotNull();
    }

    // =========================================================================
    // obtenirNomOrganisation — lambda$1 ligne 367 : .map(org -> org.getNom())
    // =========================================================================

    @Test
    @DisplayName("calculerTendance avec organisation trouvée en DB couvre lambda .map(org -> org.getNom())")
    void calculerTendance_withExistingOrganisation_returnsNomOrganisation() {
        UUID organisationId = UUID.randomUUID();

        Organisation org = new Organisation();
        org.setNom("Association Tendance Test");
        org.setEmail("tendance@test.com");
        org.setTypeOrganisation("ASSOCIATION");
        org.setStatut("ACTIVE");
        org.setActif(true);

        // Surcharger le mock par défaut : l'organisation EST trouvée → .map(org -> org.getNom()) exécuté
        when(organisationRepository.findByIdOptional(eq(organisationId)))
                .thenReturn(Optional.of(org));

        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.CE_MOIS, organisationId);

        assertThat(response).isNotNull();
        assertThat(response.getNomOrganisation()).isEqualTo("Association Tendance Test");
        assertThat(response.getOrganisationId()).isEqualTo(organisationId);
    }

    // =========================================================================
    // StatistiquesDTO — constructeur 6-args (ligne 391)
    // déclenché quand les points de données contiennent des valeurs non-nulles variées
    // =========================================================================

    @Test
    @DisplayName("calculerStatistiques avec liste vide déclenche le constructeur no-arg de StatistiquesDTO")
    void calculerStatistiques_emptyList_triggersStatistiquesDTONoArgConstructor() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "calculerStatistiques", java.util.List.class);
        method.setAccessible(true);
        Object result = method.invoke(trendService, java.util.Collections.emptyList());
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("calculerTendance avec valeurs variées déclenche le constructeur StatistiquesDTO 6-args")
    void calculerTendance_withVariedValues_triggersStatistiquesDTOFullConstructor() {
        UUID organisationId = UUID.randomUUID();

        // Des valeurs très différentes garantissent écartType != 0 → constructeur 6-args de StatistiquesDTO
        when(kpiCalculatorService.calculerTousLesKPI(eq(organisationId), any(), any()))
                .thenReturn(Map.of(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("10")))
                .thenReturn(Map.of(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("500")))
                .thenReturn(Map.of(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("250")));

        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.TROIS_DERNIERS_MOIS, organisationId);

        assertThat(response).isNotNull();
        assertThat(response.getEcartType()).isNotNull();
        assertThat(response.getValeurMinimale()).isNotNull();
        assertThat(response.getValeurMaximale()).isNotNull();
        assertThat(response.getValeurMoyenne()).isNotNull();
    }

    // =========================================================================
    // calculerTendanceLineaire — liste < 2 points (via réflexion)
    // =========================================================================

    @Test
    @DisplayName("calculerTendanceLineaire avec liste vide retourne pente=0 et corrélation=0")
    void calculerTendanceLineaire_emptyList_returnsZeros() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "calculerTendanceLineaire", java.util.List.class);
        method.setAccessible(true);
        Object result = method.invoke(trendService, java.util.Collections.emptyList());
        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("calculerTendanceLineaire avec 1 point retourne pente=0 et corrélation=0")
    void calculerTendanceLineaire_singlePoint_returnsZeros() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "calculerTendanceLineaire", java.util.List.class);
        method.setAccessible(true);
        KPITrendResponse.PointDonneeDTO point = KPITrendResponse.PointDonneeDTO.builder()
                .date(java.time.LocalDateTime.now())
                .valeur(new BigDecimal("100"))
                .libelle("T1")
                .anomalie(false)
                .prediction(false)
                .build();
        Object result = method.invoke(trendService, java.util.List.of(point));
        assertThat(result).isNotNull();
    }

    // =========================================================================
    // calculerTendanceLineaire — branches denominateur = 0 (ligne 212) et denominateurR = 0 (lignes 222-230)
    // Branche denominateur = 0 : n*sommeX2 - sommeX^2 = 0 (tous X identiques, e.g. n=1 impossible ici)
    // Pour 2 points identiques, sommeX={0,1}, sommeX2={0,1}, n*sommeX2-sommeX^2 = 2*1-1=1 ≠ 0
    // Pour atteindre denominateur=0 avec >= 2 points, il faut que les X soient tous égaux, impossible ici
    // (X = index 0,1,2,...). Donc on couvre via denominateurR = 0 :
    // denominateurR2 = n*sommeY2 - sommeY^2 = 0 quand toutes les Y sont identiques
    // =========================================================================

    @Test
    @DisplayName("calculerTendanceLineaire avec toutes les valeurs Y identiques — denominateurR2=0 → corrélation=0 (branche ligne 222-223)")
    void calculerTendanceLineaire_identicalYValues_denominateurRZero_correlationZero() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "calculerTendanceLineaire", java.util.List.class);
        method.setAccessible(true);

        // 3 points avec Y identique = 100 → denominateurR2 = 3*(100^2+100^2+100^2) - (300)^2 = 0
        // → branche: if (denominateurR1 != 0 && denominateurR2 != 0) → FAUX → coefficientCorrelation=0
        java.util.List<KPITrendResponse.PointDonneeDTO> points = java.util.List.of(
                KPITrendResponse.PointDonneeDTO.builder()
                        .date(java.time.LocalDateTime.now().minusDays(2))
                        .valeur(new BigDecimal("100"))
                        .libelle("T1")
                        .anomalie(false)
                        .prediction(false)
                        .build(),
                KPITrendResponse.PointDonneeDTO.builder()
                        .date(java.time.LocalDateTime.now().minusDays(1))
                        .valeur(new BigDecimal("100")) // même valeur
                        .libelle("T2")
                        .anomalie(false)
                        .prediction(false)
                        .build(),
                KPITrendResponse.PointDonneeDTO.builder()
                        .date(java.time.LocalDateTime.now())
                        .valeur(new BigDecimal("100")) // même valeur
                        .libelle("T3")
                        .anomalie(false)
                        .prediction(false)
                        .build()
        );

        Object result = method.invoke(trendService, points);

        // Résultat non null, pente = 0 (dénominateur != 0 car X varie), corrélation = 0
        assertThat(result).isNotNull();
        // Vérifie via réflexion que pente et corrélation sont des BigDecimal
        java.lang.reflect.Field penteField = result.getClass().getDeclaredField("pente");
        penteField.setAccessible(true);
        BigDecimal pente = (BigDecimal) penteField.get(result);
        assertThat(pente).isEqualByComparingTo(BigDecimal.ZERO);

        java.lang.reflect.Field corrField = result.getClass().getDeclaredField("coefficientCorrelation");
        corrField.setAccessible(true);
        BigDecimal corr = (BigDecimal) corrField.get(result);
        assertThat(corr).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("calculerTendanceLineaire avec denominateurR négatif (valeurs négatives) — branches sqrt edge case")
    void calculerTendanceLineaire_negativeValues_handlesGracefully() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "calculerTendanceLineaire", java.util.List.class);
        method.setAccessible(true);

        // Points avec valeurs très différentes → denominateur != 0, denominateurR != 0 → corrélation calculée
        // Ce test couvre la branche "if (denominateurR != 0)" ligne 227
        java.util.List<KPITrendResponse.PointDonneeDTO> points = java.util.List.of(
                KPITrendResponse.PointDonneeDTO.builder()
                        .date(java.time.LocalDateTime.now().minusDays(2))
                        .valeur(new BigDecimal("10"))
                        .libelle("T1")
                        .anomalie(false)
                        .prediction(false)
                        .build(),
                KPITrendResponse.PointDonneeDTO.builder()
                        .date(java.time.LocalDateTime.now().minusDays(1))
                        .valeur(new BigDecimal("50"))
                        .libelle("T2")
                        .anomalie(false)
                        .prediction(false)
                        .build(),
                KPITrendResponse.PointDonneeDTO.builder()
                        .date(java.time.LocalDateTime.now())
                        .valeur(new BigDecimal("90"))
                        .libelle("T3")
                        .anomalie(false)
                        .prediction(false)
                        .build()
        );

        Object result = method.invoke(trendService, points);

        assertThat(result).isNotNull();
        java.lang.reflect.Field penteField = result.getClass().getDeclaredField("pente");
        penteField.setAccessible(true);
        BigDecimal pente = (BigDecimal) penteField.get(result);
        // Tendance croissante → pente > 0
        assertThat(pente.compareTo(BigDecimal.ZERO)).isGreaterThan(0);
    }

    // =========================================================================
    // calculerPrediction — liste vide (via réflexion)
    // =========================================================================

    @Test
    @DisplayName("calculerPrediction avec liste vide retourne ZERO")
    void calculerPrediction_emptyList_returnsZero() throws Exception {
        // Need TendanceDTO — inner class, access via reflection
        Class<?> tendanceDTOClass = null;
        for (Class<?> inner : TrendAnalysisService.class.getDeclaredClasses()) {
            if (inner.getSimpleName().equals("TendanceDTO")) {
                tendanceDTOClass = inner;
                break;
            }
        }
        assertThat(tendanceDTOClass).isNotNull();
        java.lang.reflect.Constructor<?> ctor = tendanceDTOClass.getDeclaredConstructor(
                java.math.BigDecimal.class, java.math.BigDecimal.class);
        ctor.setAccessible(true);
        Object tendance = ctor.newInstance(new BigDecimal("5"), new BigDecimal("0.8"));

        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "calculerPrediction", java.util.List.class, tendanceDTOClass);
        method.setAccessible(true);
        Object result = method.invoke(trendService, java.util.Collections.emptyList(), tendance);
        assertThat(result).isEqualTo(BigDecimal.ZERO);
    }

    // =========================================================================
    // formaterLibellePoint — branche MONTHS (via réflexion)
    // =========================================================================

    @Test
    @DisplayName("formaterLibellePoint avec ChronoUnit.MONTHS retourne mois + année")
    void formaterLibellePoint_months_returnsMonthYear() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "formaterLibellePoint", java.time.LocalDateTime.class, java.time.temporal.ChronoUnit.class);
        method.setAccessible(true);
        java.time.LocalDateTime date = java.time.LocalDateTime.of(2025, 3, 15, 0, 0);
        Object result = method.invoke(trendService, date, java.time.temporal.ChronoUnit.MONTHS);
        assertThat(result).isNotNull();
        assertThat(result.toString()).contains("2025");
    }

    @Test
    @DisplayName("formaterLibellePoint avec ChronoUnit autre retourne date.toString()")
    void formaterLibellePoint_defaultUnit_returnsDateToString() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "formaterLibellePoint", java.time.LocalDateTime.class, java.time.temporal.ChronoUnit.class);
        method.setAccessible(true);
        java.time.LocalDateTime date = java.time.LocalDateTime.of(2025, 3, 15, 10, 30);
        // HOURS is a ChronoUnit that is not DAYS, WEEKS, or MONTHS → default branch
        Object result = method.invoke(trendService, date, java.time.temporal.ChronoUnit.HOURS);
        assertThat(result).isNotNull();
        assertThat(result.toString()).contains("2025");
    }

    // =========================================================================
    // determinerValeurIntervalle — branche > 100 (dureeTotal/15)
    // =========================================================================

    @Test
    @DisplayName("determinerValeurIntervalle avec dureeTotal > 100 retourne dureeTotal/15")
    void determinerValeurIntervalle_largeDureeTotal_returnsDivide() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "determinerValeurIntervalle",
                java.time.LocalDateTime.class,
                java.time.LocalDateTime.class,
                java.time.temporal.ChronoUnit.class);
        method.setAccessible(true);
        // DAYS between these two = 200 days → 200 > 100 → 200/15 = 13
        java.time.LocalDateTime debut = java.time.LocalDateTime.of(2024, 1, 1, 0, 0);
        java.time.LocalDateTime fin = java.time.LocalDateTime.of(2024, 7, 19, 0, 0); // ~200 days
        Object result = method.invoke(trendService, debut, fin, java.time.temporal.ChronoUnit.DAYS);
        assertThat(result).isNotNull();
        assertThat((Long) result).isGreaterThan(0L);
    }

    // =========================================================================
    // determinerUniteIntervalle — branche > 365 (MONTHS)
    // =========================================================================

    @Test
    @DisplayName("determinerUniteIntervalle avec plus de 365 jours retourne MONTHS")
    void determinerUniteIntervalle_moreThan365Days_returnsMonths() throws Exception {
        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "determinerUniteIntervalle",
                java.time.LocalDateTime.class,
                java.time.LocalDateTime.class);
        method.setAccessible(true);
        java.time.LocalDateTime debut = java.time.LocalDateTime.of(2022, 1, 1, 0, 0);
        java.time.LocalDateTime fin = java.time.LocalDateTime.of(2024, 6, 1, 0, 0); // ~880 days
        Object result = method.invoke(trendService, debut, fin);
        assertThat(result).isEqualTo(java.time.temporal.ChronoUnit.MONTHS);
    }

    // =========================================================================
    // verifierAlertes — branche valeur > seuilHaut (via période longue avec valeurs très élevées)
    // =========================================================================

    @Test
    @DisplayName("calculerTendance CETTE_ANNEE couvre formaterLibellePoint MONTHS et determinerUniteIntervalle MONTHS")
    void calculerTendance_cetteAnnee_coversMonthsPathInFormatAndDeterminer() {
        // CETTE_ANNEE is > 365 days → determinerUniteIntervalle returns MONTHS
        // → formaterLibellePoint is called with MONTHS → covers that branch
        KPITrendResponse response = trendService.calculerTendance(
                TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.CETTE_ANNEE, null);

        assertThat(response).isNotNull();
        assertThat(response.getPointsDonnees()).isNotEmpty();
        // At least one label should contain the year
        assertThat(response.getPointsDonnees().stream()
                .anyMatch(p -> p.getLibelle() != null && p.getLibelle().contains("2"))).isTrue();
    }

    @Test
    @DisplayName("verifierAlertes avec valeur actuelle au-dessus du seuil haut retourne true")
    void verifierAlertes_valeurAboveSeuilHaut_returnsTrue() throws Exception {
        // Build StatistiquesDTO via reflection with a very low valeurActuelle and tight bounds
        Class<?> statsDTOClass = null;
        for (Class<?> inner : TrendAnalysisService.class.getDeclaredClasses()) {
            if (inner.getSimpleName().equals("StatistiquesDTO")) {
                statsDTOClass = inner;
                break;
            }
        }
        assertThat(statsDTOClass).isNotNull();
        java.lang.reflect.Constructor<?> ctor = statsDTOClass.getDeclaredConstructor(
                BigDecimal.class, BigDecimal.class, BigDecimal.class,
                BigDecimal.class, BigDecimal.class, BigDecimal.class);
        ctor.setAccessible(true);
        // valeurActuelle=1000, moyenne=100, ecartType=10
        // seuilHaut = 100 + 10*1.5 = 115 → valeurActuelle 1000 > 115 → alerteActive=true
        Object stats = ctor.newInstance(
                new BigDecimal("1000"), // valeurActuelle
                new BigDecimal("50"),   // valeurMinimale
                new BigDecimal("200"),  // valeurMaximale
                new BigDecimal("100"),  // valeurMoyenne
                new BigDecimal("10"),   // ecartType
                new BigDecimal("0.1")); // coefficientVariation

        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "verifierAlertes", BigDecimal.class, statsDTOClass);
        method.setAccessible(true);
        Object result = method.invoke(trendService, new BigDecimal("1000"), stats);
        assertThat((Boolean) result).isTrue();
    }

    @Test
    @DisplayName("verifierAlertes avec valeur actuelle en dessous du seuil bas retourne true")
    void verifierAlertes_valeurBelowSeuilBas_returnsTrue() throws Exception {
        Class<?> statsDTOClass = null;
        for (Class<?> inner : TrendAnalysisService.class.getDeclaredClasses()) {
            if (inner.getSimpleName().equals("StatistiquesDTO")) {
                statsDTOClass = inner;
                break;
            }
        }
        java.lang.reflect.Constructor<?> ctor = statsDTOClass.getDeclaredConstructor(
                BigDecimal.class, BigDecimal.class, BigDecimal.class,
                BigDecimal.class, BigDecimal.class, BigDecimal.class);
        ctor.setAccessible(true);
        // valeurActuelle=5, moyenne=100, ecartType=10
        // seuilBas = 100 - 10*1.5 = 85 → valeurActuelle 5 < 85 → alerteActive=true
        Object stats = ctor.newInstance(
                new BigDecimal("5"),    // valeurActuelle
                new BigDecimal("5"),    // valeurMinimale
                new BigDecimal("200"),  // valeurMaximale
                new BigDecimal("100"),  // valeurMoyenne
                new BigDecimal("10"),   // ecartType
                new BigDecimal("0.1")); // coefficientVariation

        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "verifierAlertes", BigDecimal.class, statsDTOClass);
        method.setAccessible(true);
        Object result = method.invoke(trendService, new BigDecimal("5"), stats);
        assertThat((Boolean) result).isTrue();
    }

    @Test
    @DisplayName("verifierAlertes avec valeur dans les bornes retourne false")
    void verifierAlertes_valeurInBounds_returnsFalse() throws Exception {
        Class<?> statsDTOClass = null;
        for (Class<?> inner : TrendAnalysisService.class.getDeclaredClasses()) {
            if (inner.getSimpleName().equals("StatistiquesDTO")) {
                statsDTOClass = inner;
                break;
            }
        }
        java.lang.reflect.Constructor<?> ctor = statsDTOClass.getDeclaredConstructor(
                BigDecimal.class, BigDecimal.class, BigDecimal.class,
                BigDecimal.class, BigDecimal.class, BigDecimal.class);
        ctor.setAccessible(true);
        // valeurActuelle=100, moyenne=100, ecartType=10
        // seuilBas=85, seuilHaut=115 → valeurActuelle=100 is between → alerteActive=false
        Object stats = ctor.newInstance(
                new BigDecimal("100"),  // valeurActuelle
                new BigDecimal("80"),   // valeurMinimale
                new BigDecimal("120"),  // valeurMaximale
                new BigDecimal("100"),  // valeurMoyenne
                new BigDecimal("10"),   // ecartType
                new BigDecimal("0.1")); // coefficientVariation

        java.lang.reflect.Method method = TrendAnalysisService.class.getDeclaredMethod(
                "verifierAlertes", BigDecimal.class, statsDTOClass);
        method.setAccessible(true);
        Object result = method.invoke(trendService, new BigDecimal("100"), stats);
        assertThat((Boolean) result).isFalse();
    }
}
