using MeterVault.Core.Analysis; using static MeterVault.Core.Tests.Analysis.AnalysisClock; namespace MeterVault.Core.Tests.Analysis; /// /// The legacy entry points' ranges (D-45): the REST API's exact instants and the older services' date ranges become /// ordinary resolved periods, cut at now, that the bucket planner tiles exactly. /// public sealed class LegacyPeriodsTests { private static readonly DateTimeOffset Now = At(Berlin, 2026, 9, 19, 14, 37); [Fact] public void Utc_midnights_keep_their_exact_instants_in_a_zone_ahead_of_utc() { // The contract test's bounds: 2024-01-01T00:00Z is 01:00 in Berlin, and 2024-03-01T00:00Z is 01:00 on 1 March. var period = LegacyPeriods.FromInstants(Utc(2024, 1, 1), Utc(2024, 3, 1), Now, Berlin); Assert.Equal((Day(2024, 1, 1), Day(2024, 3, 1)), (period.FirstDay, period.LastDay)); Assert.Equal((Utc(2024, 1, 1), Utc(2024, 3, 1)), (period.From, period.To)); Assert.False(period.IsToDate); Assert.False(period.HasNotStarted()); var plan = BucketPlanner.Plan(period, BucketSize.Month); // January from 01:00, February whole, and the first hour of 1 March — the buckets tile [from, to) exactly. Assert.Equal(3, plan.Buckets.Count); Assert.Equal(Utc(2024, 1, 1), plan.Buckets[0].From); Assert.Equal(At(Berlin, 2024, 2, 1), plan.Buckets[0].To); Assert.Equal((At(Berlin, 2024, 3, 1), Utc(2024, 3, 1)), (plan.Buckets[2].From, plan.Buckets[2].To)); Assert.Equal([Day(2024, 1, 1), Day(2024, 2, 1), Day(2024, 3, 1)], plan.Buckets.Select(LegacyPeriods.KeyOf)); } [Fact] public void Local_midnights_give_whole_months() { var period = LegacyPeriods.FromInstants(At(Berlin, 2024, 1, 1), At(Berlin, 2024, 3, 1), Now, Berlin); Assert.Equal((Day(2024, 1, 1), Day(2024, 2, 29)), (period.FirstDay, period.LastDay)); var plan = BucketPlanner.Plan(period, BucketSize.Month); Assert.Equal(2, plan.Buckets.Count); Assert.Equal(period, PeriodResolver.Resolve(PeriodPreset.Custom, Day(2024, 1, 1), Day(2024, 2, 29), Now, Berlin)); } [Fact] public void An_offset_is_read_as_the_instant_it_names() { var withOffset = LegacyPeriods.FromInstants( new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.FromHours(1)), new DateTimeOffset(2024, 3, 1, 0, 0, 0, TimeSpan.FromHours(1)), Now, Berlin); Assert.Equal(TimeSpan.Zero, withOffset.From.Offset); Assert.Equal(At(Berlin, 2024, 1, 1), withOffset.From); Assert.Equal(At(Berlin, 2024, 3, 1), withOffset.To); } [Fact] public void A_range_reaching_past_now_stops_at_now() { // D-04: a legacy caller asking for this month and the next still gets actuals up to now only. var period = LegacyPeriods.FromInstants(At(Berlin, 2026, 9, 1), At(Berlin, 2026, 11, 1), Now, Berlin); Assert.True(period.IsToDate); Assert.True(period.ExtendsPastNow); Assert.Equal(Now, period.To); Assert.Equal(Day(2026, 10, 31), period.LastDay); var plan = BucketPlanner.Plan(period, BucketSize.Month); var bucket = Assert.Single(plan.Buckets); Assert.Equal(Now, bucket.To); } [Fact] public void A_range_after_now_has_not_started_and_plans_nothing() { var period = LegacyPeriods.FromInstants(At(Berlin, 2026, 10, 1), At(Berlin, 2026, 11, 1), Now, Berlin); Assert.True(period.HasNotStarted()); Assert.Equal(period.From, period.To); Assert.Empty(BucketPlanner.Plan(period, BucketSize.Month).Buckets); Assert.Empty(LegacyPeriods.WholePeriodPlan(period).Buckets); } [Theory] [InlineData(0)] [InlineData(-1)] public void An_empty_or_inverted_range_plans_nothing(int days) { var from = At(Berlin, 2024, 5, 1); var period = LegacyPeriods.FromInstants(from, from.AddDays(days), Now, Berlin); Assert.True(period.HasNoHistory()); Assert.Empty(BucketPlanner.Plan(period, BucketSize.Month).Buckets); } [Fact] public void Instants_outside_the_supported_dates_are_clamped() { var period = LegacyPeriods.FromInstants(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, Now, Berlin); Assert.Equal(PeriodResolver.MinSupportedDate, period.FirstDay); Assert.Equal(At(Berlin, 1900, 1, 1), period.From); Assert.Equal(Now, period.To); Assert.Equal(PeriodResolver.MaxSupportedDate, period.LastDay); Assert.False(BucketPlanner.Plan(period, BucketSize.Year).Refused); } [Fact] public void A_date_range_ends_the_day_before_its_exclusive_end_and_is_cut_at_now() { // The dashboard's "this year": [1 January, 1 January next year). var year = LegacyPeriods.FromDates(Day(2026, 1, 1), Day(2027, 1, 1), Now, Berlin); Assert.Equal((Day(2026, 1, 1), Day(2026, 12, 31)), (year.FirstDay, year.LastDay)); Assert.Equal(At(Berlin, 2026, 1, 1), year.From); Assert.Equal(Now, year.To); Assert.Equal(BucketSize.Year, PeriodBucket.Of(year).Size); var previous = LegacyPeriods.FromDates(Day(2025, 1, 1), Day(2026, 1, 1), Now, Berlin); Assert.False(previous.IsToDate); Assert.Equal(At(Berlin, 2026, 1, 1), previous.To); } [Fact] public void A_date_range_in_a_zone_behind_utc_starts_at_local_midnight() { var period = LegacyPeriods.FromDates(Day(2026, 7, 1), Day(2026, 8, 1), Now, NewYork); Assert.Equal(Utc(2026, 7, 1, 4), period.From); Assert.Equal(Utc(2026, 8, 1, 4), period.To); } [Fact] public void An_empty_date_range_or_one_outside_the_supported_dates_is_handled() { Assert.True(LegacyPeriods.FromDates(Day(2026, 5, 1), Day(2026, 5, 1), Now, Berlin).HasNoHistory()); Assert.True(LegacyPeriods.FromDates(Day(2026, 5, 2), Day(2026, 5, 1), Now, Berlin).HasNoHistory()); var wide = LegacyPeriods.FromDates(new DateOnly(1, 1, 1), new DateOnly(9999, 1, 1), Now, Berlin); Assert.Equal((PeriodResolver.MinSupportedDate, PeriodResolver.MaxSupportedDate), (wide.FirstDay, wide.LastDay)); } [Fact] public void The_whole_period_plan_is_the_period_as_one_bucket() { var period = LegacyPeriods.FromDates(Day(2025, 1, 1), Day(2026, 1, 1), Now, Berlin); var plan = LegacyPeriods.WholePeriodPlan(period); Assert.False(plan.Refused); Assert.Equal(BucketSize.Year, plan.Size); Assert.Equal(PeriodBucket.Of(period), Assert.Single(plan.Buckets)); } [Theory] [InlineData(BucketSize.Day, "2026-09-17")] [InlineData(BucketSize.Week, "2026-09-14")] [InlineData(BucketSize.Month, "2026-09-01")] [InlineData(BucketSize.Year, "2026-01-01")] public void A_bucket_is_filed_under_the_start_of_its_calendar_unit(BucketSize size, string key) { var bucket = new AnalysisBucket(Day(2026, 9, 17), Day(2026, 9, 18), At(Berlin, 2026, 9, 17), At(Berlin, 2026, 9, 18), size); Assert.Equal(Iso(key), LegacyPeriods.KeyOf(bucket)); } }