using App.Core.Entities.Maintenance; using App.Core.Services.Maintenance; namespace App.Core.Tests.Maintenance; public class FrequencyCalculatorTests { private static MaintenancePlan CreatePlan(FrequencyType type, DateOnly startDate) => new() { MachineId = 1, Name = "Test", FrequencyType = type, StartDate = startDate, }; [Fact] public void ComputeNextDueDate_Daily_ReturnsAnchorPlusOneDay() { var plan = CreatePlan(FrequencyType.Daily, new DateOnly(2026, 3, 10)); var next = FrequencyCalculator.ComputeNextDueDate(plan); Assert.Equal(new DateOnly(2026, 3, 11), next); } [Fact] public void ComputeNextDueDate_UsesLastExecutionDate_WhenPlanAlreadyRanOnce() { var plan = CreatePlan(FrequencyType.Daily, new DateOnly(2026, 1, 1)); plan.LastExecutionDateUtc = new DateOnly(2026, 3, 10); var next = FrequencyCalculator.ComputeNextDueDate(plan); Assert.Equal(new DateOnly(2026, 3, 11), next); } [Fact] public void ComputeNextDueDate_Weekly_ReturnsNextConfiguredWeekday() { // 2026-03-10 est un mardi. Gamme hebdomadaire calée sur le lundi (1). var plan = CreatePlan(FrequencyType.Weekly, new DateOnly(2026, 3, 10)); plan.DayOfWeek = (int)DayOfWeek.Monday; var next = FrequencyCalculator.ComputeNextDueDate(plan); Assert.Equal(new DateOnly(2026, 3, 16), next); // premier lundi après le mardi 10/03 Assert.Equal(DayOfWeek.Monday, next.DayOfWeek); } [Fact] public void ComputeNextDueDate_Weekly_MissingDayOfWeek_Throws() { var plan = CreatePlan(FrequencyType.Weekly, new DateOnly(2026, 3, 10)); Assert.Throws(() => FrequencyCalculator.ComputeNextDueDate(plan)); } [Fact] public void ComputeNextDueDate_Monthly_ClampsToLastDayOfShorterMonth() { // Ancre en janvier, jour du mois = 31 : février n'a pas 31 jours -> clampé au 28/29. var plan = CreatePlan(FrequencyType.Monthly, new DateOnly(2026, 1, 15)); plan.DayOfMonth = 31; var next = FrequencyCalculator.ComputeNextDueDate(plan); Assert.Equal(new DateOnly(2026, 2, 28), next); // 2026 n'est pas bissextile } [Fact] public void ComputeNextDueDate_MonthlyRule_FirstMonday() { // Ancre en janvier -> cible février 2024. Le 1er lundi de février 2024 est le 05/02. var plan = CreatePlan(FrequencyType.MonthlyRule, new DateOnly(2024, 1, 10)); plan.MonthlyRuleOccurrence = 1; plan.MonthlyRuleWeekday = (int)DayOfWeek.Monday; var next = FrequencyCalculator.ComputeNextDueDate(plan); Assert.Equal(new DateOnly(2024, 2, 5), next); } [Fact] public void ComputeNextDueDate_MonthlyRule_LastFriday() { // Cible février 2024 (bissextile). Le dernier vendredi de février 2024 est le 23/02. var plan = CreatePlan(FrequencyType.MonthlyRule, new DateOnly(2024, 1, 10)); plan.MonthlyRuleOccurrence = -1; plan.MonthlyRuleWeekday = (int)DayOfWeek.Friday; var next = FrequencyCalculator.ComputeNextDueDate(plan); Assert.Equal(new DateOnly(2024, 2, 23), next); } [Theory] [InlineData(FrequencyType.Bimonthly, 2)] [InlineData(FrequencyType.Quarterly, 3)] [InlineData(FrequencyType.Semiannual, 6)] [InlineData(FrequencyType.Annual, 12)] public void ComputeNextDueDate_FixedMonthIntervals_AddsExpectedMonths(FrequencyType type, int months) { var anchor = new DateOnly(2026, 3, 10); var plan = CreatePlan(type, anchor); var next = FrequencyCalculator.ComputeNextDueDate(plan); Assert.Equal(anchor.AddMonths(months), next); } [Fact] public void ComputeNextDueDate_CustomDays_AddsConfiguredInterval() { var plan = CreatePlan(FrequencyType.CustomDays, new DateOnly(2026, 3, 10)); plan.CustomIntervalDays = 45; var next = FrequencyCalculator.ComputeNextDueDate(plan); Assert.Equal(new DateOnly(2026, 4, 24), next); } [Fact] public void ComputeNextDueDate_CounterBased_ThrowsNotSupported() { var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10)); Assert.Throws(() => FrequencyCalculator.ComputeNextDueDate(plan)); } [Fact] public void ComputeNextCounterThreshold_NoPriorExecution_StartsFromZero() { var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10)); plan.CounterThreshold = 500; var threshold = FrequencyCalculator.ComputeNextCounterThreshold(plan); Assert.Equal(500, threshold); } [Fact] public void ComputeNextCounterThreshold_AddsToLastExecutionValue() { var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10)); plan.CounterThreshold = 500; plan.LastExecutionCounterValue = 1200; var threshold = FrequencyCalculator.ComputeNextCounterThreshold(plan); Assert.Equal(1700, threshold); } [Fact] public void IsCounterWithinLeadWindow_TrueOncePercentReached() { var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10)); plan.CounterThreshold = 100; plan.LastExecutionCounterValue = 1000; plan.LeadTimePercentForCounter = 90; Assert.False(FrequencyCalculator.IsCounterWithinLeadWindow(plan, currentCounterValue: 1085, defaultLeadTimePercent: 80)); Assert.True(FrequencyCalculator.IsCounterWithinLeadWindow(plan, currentCounterValue: 1090, defaultLeadTimePercent: 80)); } [Fact] public void IsCounterWithinLeadWindow_UsesDefaultPercent_WhenPlanDoesNotOverride() { var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10)); plan.CounterThreshold = 100; Assert.True(FrequencyCalculator.IsCounterWithinLeadWindow(plan, currentCounterValue: 81, defaultLeadTimePercent: 80)); Assert.False(FrequencyCalculator.IsCounterWithinLeadWindow(plan, currentCounterValue: 79, defaultLeadTimePercent: 80)); } }