using MeterVault.Core.Analysis;
using MeterVault.Infrastructure.Dashboard;
namespace MeterVault.Integration.Tests.Specialized;
///
/// The pure arithmetic behind the Solar view's derived figures (brief §4.3, A05): an input without data makes the bucket
/// unknown while an observed zero is a valid input; partial stays partial; a total never subtracts two totals over
/// different stretches of time; units are never mixed (D-20); a change needs the same buckets on both sides (D-07).
///
public sealed class SolarFiguresTests
{
private static BucketValue Ok(double value) => BucketValue.Available(value, Provenance.Imported);
private static BucketValue Part(double value) => new(value, BucketStatus.Partial, Provenance.Imported, ValueIssue.PartialCoverage);
private static readonly BucketValue None = BucketValue.Missing();
[Fact]
public void An_observed_zero_is_an_input_and_a_missing_value_is_not()
{
var zero = SolarService.Combine(Ok(300), Ok(0), -1);
Assert.Equal((300d, BucketStatus.Available), (zero.Value, zero.Status));
Assert.True(zero.Provenance.HasFlag(Provenance.Derived));
var missing = SolarService.Combine(Ok(300), None, -1);
Assert.Equal(((double?)null, BucketStatus.Missing, ValueIssue.MissingSource), (missing.Value, missing.Status, missing.Issue));
var neither = SolarService.Combine(None, None, -1);
Assert.Equal((BucketStatus.Missing, ValueIssue.NoCoverage), (neither.Status, neither.Issue));
var partial = SolarService.Combine(Part(300), Ok(100), -1);
Assert.Equal((200d, BucketStatus.Partial), (partial.Value, partial.Status));
// Being prepared or too coarse decides the status, even beside a missing input.
var pending = SolarService.Combine(None, new BucketValue(null, BucketStatus.Pending, Provenance.None, ValueIssue.AnalysisPending), -1);
Assert.Equal(BucketStatus.Pending, pending.Status);
var coarse = SolarService.Combine(new BucketValue(null, BucketStatus.Unresolved, Provenance.Imported, ValueIssue.CoarseResolution), Ok(1), 1);
Assert.Equal((BucketStatus.Unresolved, ValueIssue.CoarseResolution), (coarse.Status, coarse.Issue));
}
[Fact]
public void A_total_is_taken_over_the_buckets_both_inputs_know_unless_both_are_complete()
{
// Load known February and March (700), import known January to March (800): the difference of the totals would
// be −100; over the months both know it is 300, partial.
List load = [None, Ok(400), Ok(300)];
List import = [Ok(400), Ok(300), Ok(100)];
var a = new SolarFigure(SolarBasis.Measured, "kWh", load, Part(700)) { InputUnits = ["kWh"] };
var b = new SolarFigure(SolarBasis.Measured, "kWh", import, Ok(800)) { InputUnits = ["kWh"] };
var self = SolarService.Derive(SolarBasis.LoadMinusImport, a, b, -1);
Assert.Equal([null, 100d, 200d], self.Values.Select(v => v.Value));
Assert.Equal((300d, BucketStatus.Partial), (self.Total.Value, self.Total.Status));
// Both complete: the totals decide (a monthly import resolves the period even where it does not resolve days).
var whole = SolarService.CombineTotal(Ok(700), Ok(400), -1, [new BucketValue(null, BucketStatus.Unresolved, Provenance.None)]);
Assert.Equal((300d, BucketStatus.Available), (whole.Value, whole.Status));
}
[Fact]
public void Figures_in_different_units_are_never_combined()
{
var generation = new SolarFigure(SolarBasis.Measured, "Wh", [Ok(5000)], Ok(5000)) { InputUnits = ["Wh"] };
var export = new SolarFigure(SolarBasis.Measured, "kWh", [Ok(1)], Ok(1)) { InputUnits = ["kWh"] };
var self = SolarService.Derive(SolarBasis.GenerationMinusExport, generation, export, -1);
Assert.True(self.UnitsDiffer);
Assert.Equal(["Wh", "kWh"], self.InputUnits);
Assert.Equal(((double?)null, BucketStatus.Invalid), (self.Total.Value, self.Total.Status));
Assert.Equal(BucketStatus.Invalid, SolarService.Share(self, generation).Status);
Assert.Equal(BucketStatus.Invalid, SolarService.Share(export, generation).Status);
}
[Fact]
public void A_share_needs_a_positive_base_and_says_when_it_is_partial()
{
var part = new SolarFigure(SolarBasis.Measured, "kWh", [Ok(30), None], Part(30)) { InputUnits = ["kWh"] };
var whole = new SolarFigure(SolarBasis.Measured, "kWh", [Ok(60), Ok(40)], Ok(100)) { InputUnits = ["kWh"] };
var share = SolarService.Share(part, whole);
Assert.Equal((50d, BucketStatus.Partial), (share.Value, share.Status));
var zero = new SolarFigure(SolarBasis.Measured, "kWh", [Ok(0)], Ok(0)) { InputUnits = ["kWh"] };
Assert.Equal(BucketStatus.Invalid, SolarService.Share(zero, zero).Status);
}
[Fact]
public void A_change_compares_the_same_complete_buckets_on_both_sides()
{
// Complete totals: between them. Otherwise only the pairs complete on both sides (here the first).
Assert.Equal(20, SolarService.MatchedChange(Ok(120), [Ok(120)], Ok(100), [Ok(100)]).Absolute);
var matched = SolarService.MatchedChange(Part(150), [Ok(120), Part(30)], Ok(200), [Ok(100), Ok(100)]);
Assert.Equal((20d, 20d), (matched.Absolute, matched.Percent));
Assert.False(SolarService.MatchedChange(Part(30), [None, Part(30)], Ok(200), [Ok(100), Ok(100)]).IsAvailable);
Assert.False(SolarService.MatchedChange(Ok(1), [Ok(1)], null, null).IsAvailable);
}
}