using MeterVault.App.Analysis;
using MeterVault.App.MeterDetails;
using MeterVault.Core.Analysis;
using MeterVault.Core.Analysis.Coverage;
using static MeterVault.Integration.Tests.Analysis.AnalysisUiTestData;
namespace MeterVault.Integration.Tests.Analysis;
///
/// Where the shared components lead (D-48, D-51, brief §3.1, §4.3, §5.1): drilling into a bucket keeps the scope and
/// opens the next finer bucket the data supports, "go to latest data" keeps the kind of period, breadcrumbs carry the
/// period up, and a formula names its meters. Pure; no database.
///
public sealed class AnalysisNavigationTests
{
private static readonly AnalysisQuery History = AnalysisQuery.Default(AnalysisDefaults.History);
[Fact]
public void A_year_opens_its_months_and_a_month_its_days()
{
var year = Buckets(D(2025, 1, 1), D(2025, 12, 31), BucketSize.Year)[0];
var month = Buckets(D(2026, 2, 1), D(2026, 2, 28))[0];
var intoYear = AnalysisNavigation.DrillInto(History, year)!;
Assert.Equal((D(2025, 1, 1), D(2025, 12, 31)), (intoYear.From!.Value, intoYear.To!.Value));
Assert.Equal(BucketSize.Month, intoYear.Bucket);
Assert.Equal(History.Comparison, intoYear.Comparison);
var intoMonth = AnalysisNavigation.DrillInto(History, month)!;
Assert.Equal((D(2026, 2, 1), D(2026, 2, 28)), (intoMonth.From!.Value, intoMonth.To!.Value));
Assert.Equal(BucketSize.Day, intoMonth.Bucket);
}
[Fact]
public void Only_a_bucket_the_data_resolves_is_offered()
{
var month = Buckets(D(2026, 2, 1), D(2026, 2, 28))[0];
var day = Buckets(D(2026, 2, 3), D(2026, 2, 3), BucketSize.Day)[0];
Assert.Equal(BucketSize.Week, AnalysisNavigation.DrillInto(History, month, ResolutionClass.Week)!.Bucket);
Assert.Equal(BucketSize.Day, AnalysisNavigation.DrillInto(History, month, ResolutionClass.Hour)!.Bucket);
// A monthly import has nothing finer than its month, and a day nothing finer than itself: open the records.
Assert.Null(AnalysisNavigation.DrillInto(History, month, ResolutionClass.Month));
Assert.Null(AnalysisNavigation.DrillInto(History, day));
Assert.Equal("/meters/5?tab=normalized&from=2026-02-01&to=2026-02-28", AnalysisNavigation.NormalizedData(5, History, month));
}
[Fact]
public void A_virtual_meters_month_that_has_nothing_finer_opens_as_its_own_month_never_a_dead_end()
{
// Brief §3.2 "Explain a spike" for a virtual meter over monthly sources: a month cannot open days, and a virtual
// meter has no records, so the month opens the meter's analysis over that month — whose source contributions link
// to each source's records for it. On that one-month view the month leads nowhere, and nothing invites a click.
var year = AnalysisQuery.Parse("?period=prev-year", MeterAnalysisLoader.DefaultsFor(9));
var shownYear = Range(D(2025, 1, 1), D(2025, 12, 31));
var march = Buckets(D(2025, 1, 1), D(2025, 12, 31))[2];
Assert.Equal(
"/meters/9?tab=analysis&from=2025-03-01&to=2025-03-31",
MeterDrill.Href(9, isVirtual: true, year, shownYear, march, ResolutionClass.Month));
var month = year.WithCustomRange(D(2025, 3, 1), D(2025, 3, 31));
var onlyMarch = Buckets(D(2025, 3, 1), D(2025, 3, 31))[0];
Assert.Null(MeterDrill.Href(9, isVirtual: true, month, Range(D(2025, 3, 1), D(2025, 3, 31)), onlyMarch, ResolutionClass.Month));
// A physical meter's month opens its records, and a finer resolution still opens days.
Assert.Equal(
"/meters/4?tab=normalized&from=2025-03-01&to=2025-03-31",
MeterDrill.Href(4, isVirtual: false, year, shownYear, march, ResolutionClass.Month));
Assert.Equal(
"/meters/9?tab=analysis&from=2025-03-01&to=2025-03-31&bucket=day",
MeterDrill.Href(9, isVirtual: true, year, shownYear, march, ResolutionClass.Day));
}
[Fact]
public void The_month_cut_at_now_opens_as_the_whole_month()
{
var period = PeriodResolver.Resolve(PeriodPreset.Last12Months, null, null, Now, Berlin);
var current = BucketPlanner.Plan(period, BucketSize.Month).Buckets[^1];
Assert.True(current.IsCutShort);
var drilled = AnalysisNavigation.DrillInto(History, current)!;
Assert.Equal((D(2026, 9, 1), D(2026, 9, 30)), (drilled.From!.Value, drilled.To!.Value));
}
[Fact]
public void A_named_year_comparison_becomes_the_year_before_below_a_year()
{
var named = History.WithCustomRange(D(2025, 1, 1), D(2025, 12, 31)).WithComparison(new ComparisonRequest(ComparisonKind.Year, 2023));
var month = Buckets(D(2025, 3, 1), D(2025, 3, 31))[0];
var year = Buckets(D(2025, 1, 1), D(2025, 12, 31), BucketSize.Year)[0];
Assert.Equal(ComparisonKind.PreviousYear, AnalysisNavigation.DrillInto(named, month)!.Comparison.Kind);
Assert.Equal(new ComparisonRequest(ComparisonKind.Year, 2023), AnalysisNavigation.DrillInto(named, year)!.Comparison);
}
[Theory]
[InlineData(PeriodPreset.MonthToDate, "2023-03-01", "2023-03-31")]
[InlineData(PeriodPreset.LastMonth, "2023-03-01", "2023-03-31")]
[InlineData(PeriodPreset.YearToDate, "2023-01-01", "2023-12-31")]
[InlineData(PeriodPreset.PreviousYear, "2023-01-01", "2023-12-31")]
[InlineData(PeriodPreset.Last12Months, "2022-04-01", "2023-03-31")]
[InlineData(PeriodPreset.Last24Months, "2021-04-01", "2023-03-31")]
public void Latest_data_keeps_the_kind_of_period(PeriodPreset preset, string first, string last)
{
var availability = new AvailableRange(Now, Now, D(2020, 1, 1), D(2023, 3, 15));
var latest = AnalysisNavigation.LatestData(History.WithPeriod(preset), availability)!;
Assert.True(latest.IsCustom);
Assert.Equal(DateOnly.Parse(first, System.Globalization.CultureInfo.InvariantCulture), latest.From);
Assert.Equal(DateOnly.Parse(last, System.Globalization.CultureInfo.InvariantCulture), latest.To);
}
[Fact]
public void Latest_data_of_a_custom_range_keeps_its_length_and_needs_availability()
{
var availability = new AvailableRange(Now, Now, D(2020, 1, 1), D(2023, 3, 15));
var tenDays = History.WithCustomRange(D(2026, 9, 1), D(2026, 9, 10));
var latest = AnalysisNavigation.LatestData(tenDays, availability)!;
Assert.Equal((D(2023, 3, 6), D(2023, 3, 15)), (latest.From!.Value, latest.To!.Value));
Assert.Null(AnalysisNavigation.LatestData(tenDays, null));
}
[Fact]
public void Breadcrumbs_carry_the_period_up_and_end_at_the_current_page() => In("en", () =>
{
var ytd = AnalysisQuery.Default(AnalysisDefaults.History.ForScope(QueryScope.ForMeter(5))).WithPeriod(PeriodPreset.YearToDate);
var meterPage = AnalysisNavigation.Breadcrumbs(ytd, (1, "Strom"), (5, "Auto"));
Assert.Equal(
[new Crumb("Overview", "/?period=ytd"), new Crumb("Strom", "/energy/1?period=ytd"), new Crumb("Auto", null)],
meterPage);
var below = AnalysisNavigation.Breadcrumbs(ytd, (1, "Strom"), (5, "Auto"), "Normalized data");
Assert.Equal("/meters/5?tab=analysis&period=ytd", below[2].Href);
Assert.Equal(new Crumb("Normalized data", null), below[3]);
// From the Overview's month to date: the Overview's own default is not written, a history page's is.
var mtd = AnalysisQuery.Default(AnalysisDefaults.Overview);
Assert.Equal([new Crumb("Overview", "/"), new Crumb("Strom", null)], AnalysisNavigation.Breadcrumbs(mtd, (1, "Strom")));
Assert.Equal("/energy/1?period=mtd", AnalysisNavigation.Breadcrumbs(mtd, (1, "Strom"), current: "Flow")[1].Href);
Assert.Equal([new Crumb("Overview", null)], AnalysisNavigation.Breadcrumbs(null));
});
[Fact]
public void A_formula_names_its_meters_beside_their_tokens()
{
var segments = FormulaText.Split("m5 + m6");
Assert.Equal([new FormulaSegment("m5", 5), new FormulaSegment(" + ", null), new FormulaSegment("m6", 6)], segments);
var mixed = FormulaText.Split("0.5*m12 - (m3/m44) + mx + m1a + M2 + 1.3m7");
Assert.Equal([12, 3, 44, 7], mixed.Where(s => s.IsMeter).Select(s => s.MeterId!.Value));
Assert.Equal("0.5*m12 - (m3/m44) + mx + m1a + M2 + 1.3m7", string.Concat(mixed.Select(s => s.Text)));
var names = new Dictionary { [5] = "Solar 1", [6] = "Solar 2" };
Assert.Equal("m5 (Solar 1) + m6 (Solar 2) - m9", FormulaText.Annotate("m5 + m6 - m9", id => names.GetValueOrDefault(id)));
Assert.Empty(FormulaText.Split(null));
Assert.Empty(FormulaText.Split(" "));
}
}