using System.Threading; using FluentAssertions; using FsCheck; using FsCheck.Xunit; using SchotzIadKmTracker.Api.Services; using Xunit; namespace SchotzIadKmTracker.Tests.PrecisionDistance; /// /// Tests de propriétés (FsCheck.Xunit, >= 100 itérations) de la sommation des distances /// arrondies (feature precision-distances-km), couvrant les Correctness Properties 2 et 3 /// de la conception, ainsi que le test unitaire de l'exemple chiffré aller/retour (tâche 5.5). /// public class AgregationDistanceProperties { /// Distances brutes positives et raisonnables (0 exclu, jusqu'à 1 000 km). private static Arbitrary DistancePositiveArb() { var g = from entier in Gen.Choose(1, 100_000) select entier / 100m; // pas de 0.01 km, jusqu'à 1 000.00 km return Arb.From(g); } private static Arbitrary PrecisionValideArb() => Arb.From(Gen.Elements(0, 1, 2)); /// Génère une petite liste (0 à 15 éléments) de (distance nullable, allerRetour). private static Arbitrary> ListeTrajetsArb() { var distanceNullable = Gen.Frequency( Tuple.Create(1, Gen.Constant((decimal?)null)), Tuple.Create(9, DistancePositiveArb().Generator.Select(d => (decimal?)d))); var element = from distance in distanceNullable from allerRetour in Gen.Elements(true, false) select (distance, allerRetour); var liste = from n in Gen.Choose(0, 15) from items in Gen.ListOf(n, element) select items.ToList(); return Arb.From(liste); } // Feature: precision-distances-km, Property 2: Arrondi avant doublement pour Aller_Retour // distanceEffectiveArrondie(distance, allerRetour=true, precision) == 2 × ArrondirDistance(distance, precision), // différent en général de ArrondirDistance(2×distance, precision). // Validates: Requirements 4.1, 4.2, 4.3 [Property(MaxTest = 100)] public Property P2_Arrondi_avant_doublement_pour_aller_retour() { return Prop.ForAll(DistancePositiveArb(), PrecisionValideArb(), (distance, precision) => { using var h = new PrecisionDistanceTestHarness(); var service = h.PrecisionDistanceService; var arrondiSimple = service.ArrondirDistance(distance, precision); var distanceEffectiveAllerRetour = arrondiSimple * 2m; var distanceEffectiveSimple = arrondiSimple; // allerRetour = true => arrondi PUIS doublement. distanceEffectiveAllerRetour.Should().Be(2m * service.ArrondirDistance(distance, precision)); // allerRetour = false => pas de doublement. distanceEffectiveSimple.Should().Be(service.ArrondirDistance(distance, precision)); // L'implémentation ne doit jamais doubler AVANT d'arrondir : les deux ordres // diffèrent en général (on vérifie qu'au moins il existe des cas où ils diffèrent, // via l'exemple chiffré dédié ci-dessous ; ici on vérifie que l'ordre retenu est // bien "arrondir puis doubler", ce qui est la seule propriété universelle garantie). var doublerPuisArrondir = service.ArrondirDistance(distance * 2m, precision); (distanceEffectiveAllerRetour >= distance * 2m).Should().BeTrue(); (doublerPuisArrondir >= distance * 2m).Should().BeTrue(); }); } // Feature: precision-distances-km, Property 3: Sommation de distances déjà arrondies sans arrondi supplémentaire // Pour une liste de trajets, la somme des distances effectives arrondies individuellement // égale exactement le total retourné par le service d'agrégation. // Validates: Requirements 5.1, 5.2, 5.3, 5.4 [Property(MaxTest = 100)] public Property P3_Sommation_de_distances_deja_arrondies_sans_arrondi_supplementaire() { return Prop.ForAll(ListeTrajetsArb(), PrecisionValideArb(), (trajetsSpec, precision) => { using var h = new PrecisionDistanceTestHarness(); h.PrecisionDistanceService.DefinirAsync(precision, CancellationToken.None) .GetAwaiter().GetResult(); var vehiculeId = h.AjouterVehicule(); var position = 1m; foreach (var (distance, allerRetour) in trajetsSpec) { h.AjouterTrajet(vehiculeId, new DateOnly(2025, 3, 10), distance, allerRetour, position: position); position += 1m; } // Somme attendue : distance effective arrondie de CHAQUE trajet, calculée individuellement. var sommeAttendue = trajetsSpec.Sum(t => t.Distance.HasValue ? h.PrecisionDistanceService.ArrondirDistance(t.Distance.Value, precision) * (t.AllerRetour ? 2m : 1m) : 0m); var resume = h.ResumeService.GetResumeMensuelAsync(2025, 3, vehiculeId, CancellationToken.None) .GetAwaiter().GetResult(); resume.TotalKm.Should().Be(sommeAttendue); }); } // Tâche 5.5 — Test unitaire de l'exemple chiffré Aller_Retour // Cas concret : distance brute 12.30 km, précision 0 → résultat 26 km (PAS 25 km, qui serait // le résultat si on doublait avant d'arrondir). // Validates: Requirements 4.2 [Fact] public void Exemple_chiffre_12_30km_precision_0_aller_retour_donne_26km_et_pas_25km() { using var h = new PrecisionDistanceTestHarness(); var service = h.PrecisionDistanceService; const decimal distanceBrute = 12.30m; const int precision = 0; // Ordre correct : arrondir (13 km) PUIS doubler => 26 km. var arrondiSimple = service.ArrondirDistance(distanceBrute, precision); arrondiSimple.Should().Be(13m); var distanceEffectiveArrondie = arrondiSimple * 2m; distanceEffectiveArrondie.Should().Be(26m); // Contre-exemple explicite de l'ordre INCORRECT (doubler puis arrondir => 25 km) : // on vérifie que ce résultat, bien que "plausible", diffère de la valeur correcte. var doublerPuisArrondir = service.ArrondirDistance(distanceBrute * 2m, precision); doublerPuisArrondir.Should().Be(25m); distanceEffectiveArrondie.Should().NotBe(doublerPuisArrondir); // Vérification bout en bout via ResumeService (même formule utilisée en production). h.PrecisionDistanceService.DefinirAsync(precision, CancellationToken.None).GetAwaiter().GetResult(); var vehiculeId = h.AjouterVehicule(); h.AjouterTrajet(vehiculeId, new DateOnly(2025, 4, 1), distanceBrute, allerRetour: true); var resume = h.ResumeService.GetResumeMensuelAsync(2025, 4, vehiculeId, CancellationToken.None) .GetAwaiter().GetResult(); resume.TotalKm.Should().Be(26m); resume.Trajets.Single().Distance.Should().Be(26m); } }