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 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 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(); } }