using MeterVault.App; using MeterVault.App.Analysis; using MeterVault.Core.Analysis; using MeterVault.Core.Domain; namespace MeterVault.Integration.Tests.Analysis; /// /// The link helpers (D-47, D-52, brief ยง3.1): stable meter tab keys with their compatibility mapping, and period-carrying /// links to the meter, energy type, analysis, specialized and export pages and the tariff editor. Pure; no database. /// public sealed class AppLinkTests { [Theory] [InlineData(null, MeterMode.CumulativeCounter, "analysis")] [InlineData("", MeterMode.CumulativeCounter, "analysis")] [InlineData("readings", MeterMode.CumulativeCounter, "readings")] [InlineData(" EVENTS ", MeterMode.CumulativeCounter, "events")] [InlineData("consumption", MeterMode.CumulativeCounter, "normalized")] [InlineData("normalized", MeterMode.GenerationCounter, "normalized")] [InlineData("tariffs", MeterMode.RuntimeCounter, "tariffs")] [InlineData("sources", MeterMode.InstantRate, "sources")] [InlineData("calculation", MeterMode.CumulativeCounter, "analysis")] [InlineData("nonsense", MeterMode.DirectDelta, "analysis")] [InlineData("readings", MeterMode.ConsumableBalance, "readings")] [InlineData("events", MeterMode.ConsumableBalance, "events")] [InlineData("sources", MeterMode.Virtual, "calculation")] [InlineData("readings", MeterMode.Virtual, "analysis")] [InlineData("consumption", MeterMode.Virtual, "analysis")] [InlineData("normalized", MeterMode.Virtual, "analysis")] [InlineData("events", MeterMode.Virtual, "events")] [InlineData("Calculation", MeterMode.Virtual, "calculation")] public void Every_tab_key_opens_a_tab_the_meter_shows(string? requested, MeterMode mode, string expected) { var resolved = MeterLinks.ResolveTab(requested, mode); Assert.Equal(expected, resolved); Assert.Contains(resolved, MeterLinks.VisibleTabs(mode)); } [Fact] public void Tabs_follow_the_mode() { Assert.Equal(["analysis", "readings", "normalized", "events", "tariffs", "sources"], MeterLinks.VisibleTabs(MeterMode.CumulativeCounter)); Assert.Equal(["analysis", "readings", "normalized", "events", "tariffs", "sources"], MeterLinks.VisibleTabs(MeterMode.ConsumableBalance)); // Calculation replaces Sources, Readings goes, Events keeps Note (D-31). Assert.Equal(["analysis", "events", "tariffs", "calculation"], MeterLinks.VisibleTabs(MeterMode.Virtual)); foreach (var mode in Enum.GetValues()) { var tabs = MeterLinks.VisibleTabs(mode); Assert.Equal(MeterLinks.TabAnalysis, tabs[0]); for (var i = 0; i < tabs.Count; i++) { Assert.Equal(i, MeterLinks.PanelIndex(tabs[i], mode)); } } Assert.Equal(3, MeterLinks.PanelIndex("sources", MeterMode.Virtual)); Assert.Equal(0, MeterLinks.PanelIndex("readings", MeterMode.Virtual)); Assert.Equal(2, MeterLinks.PanelIndex("consumption", MeterMode.CumulativeCounter)); } [Fact] public void Meter_links_carry_the_period_after_their_own_keys() { Assert.Equal("/meters/7?tab=analysis", MeterLinks.Analysis(7)); Assert.Equal("/meters/7?tab=analysis", MeterLinks.Analysis(7, AnalysisQuery.Default(AnalysisDefaults.History))); Assert.Equal("/meters/7?tab=analysis&period=mtd", MeterLinks.Analysis(7, AnalysisQuery.Default(AnalysisDefaults.Overview))); var query = AnalysisQuery.Parse("?scope=type&id=2&from=2025-01-01&to=2025-01-31&bucket=day&metric=generation", AnalysisDefaults.History); Assert.Equal( "/meters/7?tab=analysis&metric=generation&from=2025-01-01&to=2025-01-31&bucket=day", MeterLinks.Analysis(7, query)); Assert.Equal( "/meters/7?tab=normalized&metric=generation&from=2025-01-01&to=2025-01-31&bucket=day", MeterLinks.Detail(7, MeterLinks.TabNormalized, null, query)); Assert.Equal("/meters/7?tab=events&action=note", MeterLinks.Detail(7, MeterLinks.TabEvents, "note", null)); // The existing addresses are untouched. Assert.Equal("/meters/7?tab=readings&action=reading", MeterLinks.QuickEntry(7, MeterMode.CumulativeCounter)); Assert.Equal("/meters/7", MeterLinks.Detail(7)); } [Fact] public void Analysis_page_links_name_scope_and_metric() { var ytd = AnalysisQuery.Default(AnalysisDefaults.History).WithPeriod(PeriodPreset.YearToDate); Assert.Equal("/trends?scope=type&id=3&metric=cost&period=ytd", AnalysisLinks.Analysis(QueryScope.ForEnergyType(3), AnalysisMetric.Cost, ytd)); Assert.Equal("/trends", AnalysisLinks.Analysis(QueryScope.Portfolio)); Assert.Equal("/trends?scope=meters&ids=4,9", AnalysisLinks.Analysis(QueryScope.ForMeters([4, 9]))); Assert.Equal( "/trends?scope=category&id=2&metric=generation&period=ytd", AnalysisLinks.Analysis(QueryScope.ForCategory(2), query: ytd.WithMetric(AnalysisMetric.Generation))); } [Fact] public void Energy_type_and_specialized_links_carry_the_period() { var mtd = AnalysisQuery.Default(AnalysisDefaults.Overview); Assert.Equal("/energy/3", AnalysisLinks.EnergyType(3)); Assert.Equal("/energy/3", AnalysisLinks.EnergyType(3, AnalysisLinks.EnergyTabOverview)); Assert.Equal("/energy/3?tab=history&period=mtd", AnalysisLinks.EnergyType(3, "History", mtd)); Assert.Equal("/energy/3?period=mtd", AnalysisLinks.EnergyType(3, "bogus", mtd)); Assert.Equal("/solar?period=mtd", AnalysisLinks.Solar(mtd)); Assert.Equal("/consumables", AnalysisLinks.Consumables()); Assert.Equal("/", AnalysisLinks.Overview(mtd)); Assert.Equal("/?period=12m", AnalysisLinks.Overview(AnalysisQuery.Default(AnalysisDefaults.History))); Assert.Equal("overview", AnalysisLinks.ResolveEnergyTab(null)); Assert.Equal("flow", AnalysisLinks.ResolveEnergyTab(" FLOW")); Assert.Equal(3, AnalysisLinks.EnergyTabIndex("meters")); Assert.Equal(0, AnalysisLinks.EnergyTabIndex("sankey")); } [Fact] public void The_export_link_writes_the_scope_it_shows() { var meterPage = AnalysisQuery.Parse("/meters/5?tab=analysis", AnalysisDefaults.History.ForScope(QueryScope.ForMeter(5))); Assert.Equal("/export/analysis.csv?scope=meter&id=5", AnalysisLinks.Export(meterPage)); var overview = AnalysisQuery.Default(AnalysisDefaults.Overview).WithMetric(AnalysisMetric.Cost); Assert.Equal("/export/analysis.csv?metric=cost&period=mtd", AnalysisLinks.Export(overview)); } [Fact] public void Tariff_links_prefill_a_new_tariff() { Assert.Equal( "/admin/tariffs?scope=meter&id=12&component=unit-price&from=2024-01-01&action=new", TariffLinks.New(TariffScope.Meter, 12, TariffComponent.UnitPrice, new DateOnly(2024, 1, 15))); Assert.Equal( "/admin/tariffs?scope=type&id=3&component=feed-in&from=2026-02-01&action=new", TariffLinks.New(TariffScope.EnergyType, 3, TariffComponent.FeedIn, new DateOnly(2026, 2, 1))); Assert.Equal( "/admin/tariffs?scope=global&component=base-price&from=2025-06-01&action=new", TariffLinks.New(TariffScope.Global, 99, TariffComponent.BasePrice, new DateOnly(2025, 6, 30))); foreach (var scope in Enum.GetValues()) { Assert.True(TariffLinks.TryParseScope(TariffLinks.ScopeToken(scope), out var parsed)); Assert.Equal(scope, parsed); Assert.True(TariffLinks.TryParseScope(scope.ToString().ToUpperInvariant(), out parsed)); Assert.Equal(scope, parsed); } foreach (var component in Enum.GetValues()) { Assert.True(TariffLinks.TryParseComponent(TariffLinks.ComponentToken(component), out var parsed)); Assert.Equal(component, parsed); Assert.True(TariffLinks.TryParseComponent(component.ToString(), out parsed)); Assert.Equal(component, parsed); } Assert.False(TariffLinks.TryParseComponent("price", out _)); Assert.False(TariffLinks.TryParseScope(null, out _)); } }