using MeterVault.Core.Analysis;
namespace MeterVault.Core.Tests.Analysis;
///
/// One rule for change figures everywhere (D-08): the absolute difference whenever both values are known,
/// a percentage only against a positive baseline, and "unknown" never read as zero.
///
public sealed class ChangeTests
{
[Fact]
public void A_rise_from_a_positive_baseline_has_an_absolute_difference_a_percentage_and_an_upward_direction()
{
var change = Change.Between(120, 100);
Assert.Equal(20, change.Absolute!.Value, 9);
Assert.Equal(20, change.Percent!.Value, 9);
Assert.Equal(1, change.Direction);
Assert.True(change.IsAvailable);
Assert.True(change.PercentApplicable);
}
[Fact]
public void A_fall_is_negative_in_both_figures()
{
var change = Change.Between(75, 100);
Assert.Equal(-25, change.Absolute!.Value, 9);
Assert.Equal(-25, change.Percent!.Value, 9);
Assert.Equal(-1, change.Direction);
}
[Fact]
public void Against_a_zero_baseline_the_percentage_is_not_applicable_but_the_difference_is_shown()
{
// The old dashboard chip read "+0.0 %" here.
var change = Change.Between(120, 0);
Assert.Equal(120, change.Absolute!.Value, 9);
Assert.Null(change.Percent);
Assert.False(change.PercentApplicable);
Assert.Equal(1, change.Direction);
}
[Fact]
public void Against_a_negative_baseline_the_percentage_is_not_applicable()
{
// A credit shrinking from -10 to -5 is a rise of 5; dividing by -10 would call it -50 %.
var change = Change.Between(-5, -10);
Assert.Equal(5, change.Absolute!.Value, 9);
Assert.Null(change.Percent);
Assert.Equal(1, change.Direction);
}
[Fact]
public void A_signed_change_from_positive_to_negative_keeps_its_percentage()
{
var change = Change.Between(-50, 100);
Assert.Equal(-150, change.Absolute!.Value, 9);
Assert.Equal(-150, change.Percent!.Value, 9);
Assert.Equal(-1, change.Direction);
}
[Fact]
public void Equal_values_are_no_change_even_after_floating_point_noise()
{
var exact = Change.Between(100, 100);
var noisy = Change.Between(0.1 + 0.2, 0.3);
Assert.Equal(0, exact.Direction);
Assert.Equal(0, exact.Percent!.Value, 9);
Assert.Equal(0, noisy.Direction);
Assert.NotNull(noisy.Absolute);
}
[Fact]
public void A_baseline_within_the_tolerance_of_zero_counts_as_zero()
{
Assert.Null(Change.Between(5, 1e-12).Percent);
}
[Theory]
[InlineData(null, 100.0)]
[InlineData(100.0, null)]
[InlineData(null, null)]
[InlineData(double.NaN, 100.0)]
[InlineData(100.0, double.PositiveInfinity)]
public void A_missing_or_non_finite_value_makes_the_change_unavailable_not_minus_100_percent(double? current, double? previous)
{
var change = Change.Between(current, previous);
Assert.Equal(Change.Unavailable, change);
Assert.Null(change.Absolute);
Assert.Null(change.Percent);
Assert.Equal(0, change.Direction);
Assert.False(change.IsAvailable);
}
[Fact]
public void A_difference_that_overflows_is_unavailable()
{
Assert.Equal(Change.Unavailable, Change.Between(double.MaxValue, -double.MaxValue));
}
[Fact]
public void A_difference_below_the_callers_display_tolerance_has_no_direction_but_keeps_its_exact_value()
{
// 0.004 € shows as "+0.00 €"; an upward arrow beside it would claim a change nobody can see.
var cents = Change.Between(100.004, 100, tolerance: 0.005);
var noiseOnly = Change.Between(100.004, 100);
Assert.Equal(0, cents.Direction);
Assert.Equal(0.004, cents.Absolute!.Value, 9);
Assert.NotNull(cents.Percent);
Assert.Equal(1, noiseOnly.Direction);
}
[Fact]
public void A_baseline_that_displays_as_zero_under_the_callers_tolerance_has_no_percentage()
{
Assert.Null(Change.Between(5, 0.004, tolerance: 0.005).Percent);
Assert.NotNull(Change.Between(5, 0.004).Percent);
}
[Theory]
[InlineData(-0.001)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public void A_negative_or_non_finite_tolerance_is_rejected(double tolerance)
{
Assert.Throws(() => Change.Between(1, 2, tolerance));
}
[Fact]
public void An_observed_zero_now_against_a_positive_baseline_is_a_real_minus_100_percent()
{
var change = Change.Between(0, 80);
Assert.Equal(-80, change.Absolute!.Value, 9);
Assert.Equal(-100, change.Percent!.Value, 9);
Assert.Equal(-1, change.Direction);
}
}