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 _));
}
}