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