using MeterVault.Core.Domain;
using MeterVault.Core.Normalization;
using MeterVault.Infrastructure.Import;
using static MeterVault.Integration.Tests.Reconciliation.ReconciliationSupport;
namespace MeterVault.Integration.Tests.Reconciliation;
///
/// Month attribution divides an interval that crosses a month boundary. The reference sheets are
/// monthly tables whose rows each carry exactly their own month, and must never be divided, or their
/// months would silently shift and the whole golden-fixture oracle (SDD §13) would be measuring the
/// attribution instead of the normalizer.
///
///
/// The reconciliation suites already compare month by month, so a spurious division would surface
/// there as a numeric failure. This asserts the mechanism directly instead of relying on that side
/// effect — and in the instance timezone, where month boundaries sit an hour or two away from the UTC
/// midnights the importer stamps rows at.
///
public sealed class GapSplittingIsInertOnFixturesTests
{
private static readonly TimeZoneInfo Berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
[Theory]
[InlineData(ReferenceProfiles.Haus, MeterMode.CumulativeCounter)]
[InlineData(ReferenceProfiles.Netz, MeterMode.CumulativeCounter)]
[InlineData(ReferenceProfiles.Auto, MeterMode.CumulativeCounter)]
[InlineData(ReferenceProfiles.Solar1, MeterMode.GenerationCounter)]
[InlineData(ReferenceProfiles.Solar2, MeterMode.GenerationCounter)]
public void Electricity_meters_produce_exactly_one_row_per_reading_at_that_reading(int meterId, MeterMode mode)
{
var staged = Stage(ReferenceProfiles.Electricity(), Electricity);
var readings = staged.Readings.Where(r => r.MeterId == meterId).OrderBy(r => r.Time).ToList();
foreach (var zone in new[] { TimeZoneInfo.Utc, Berlin })
{
var computed = NormalizationEngine.CreateDefault().Normalize(new NormalizationContext
{
Meter = new MeterConfig { MeterId = meterId, Mode = mode, Unit = "kWh" },
Readings = readings,
TimeZone = zone,
});
Assert.True(readings.Count > 20, $"meter {meterId}: expected a real series, got {readings.Count} readings.");
Assert.Equal(readings.Count, computed.Count);
Assert.Equal(readings.Select(r => r.Time), computed.Select(c => c.Time));
}
}
[Fact]
public void No_fixture_interval_is_divided()
{
var staged = Stage(ReferenceProfiles.Electricity(), Electricity);
foreach (var group in staged.Readings.GroupBy(r => r.MeterId))
{
var readings = group.OrderBy(r => r.Time).ToList();
for (var i = 1; i < readings.Count; i++)
{
Assert.True(GapAttribution.IsMonthLabel(readings[i]), $"meter {group.Key}: {readings[i].Time:O} is not a month row.");
var segments = GapAttribution.Attribute(
GapAttribution.EffectiveTime(readings[i - 1], Berlin),
GapAttribution.EffectiveTime(readings[i], Berlin),
readings[i].Time,
1,
Berlin);
var only = Assert.Single(segments);
Assert.Equal(readings[i].Time, only.Time);
}
}
}
}