using MeterVault.Infrastructure.Costing;
using MeterVault.Infrastructure.Dashboard;
using MeterVault.Infrastructure.Import;
using MeterVault.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace MeterVault.Integration.Tests;
///
/// End-to-end M5 check: load the reference dataset, then confirm the dashboard and admin pages
/// render (server prerender) without error and show real data. Cleans up the shared container.
///
[Collection("Timescale")]
public sealed class DashboardRenderTests(TimescaleFixture fx)
{
[Fact]
public async Task Pages_render_with_reference_data()
{
using var factory = new MeterVaultAppFactory(fx.ConnectionString);
using (var scope = factory.Services.CreateScope())
{
var importer = scope.ServiceProvider.GetRequiredService();
await importer.LoadAsync(Path.Combine(AppContext.BaseDirectory, "fixtures"));
}
try
{
// The import created the reference meters and their normalized consumption.
await using (var db = fx.CreateContext())
{
Assert.Contains(await db.Meters.Select(m => m.Name).ToListAsync(), n => n == "Zähler Haus");
Assert.True(await db.Meters.CountAsync() >= 8);
Assert.True(await db.Consumption.AnyAsync());
// Regression (audit): Wasser is metered (water tariff), the Kosten Wasser column is
// NOT imported, so the category is not double-counted — Dez 2022 = 14 m³ × 5 € = 70 €.
var wasser = await db.CostCategories.FirstAsync(c => c.Name == "Wasser");
var rollup = await new CostService(fx).GetCategoryCostsAsync(
wasser.Id,
new DateTimeOffset(2022, 12, 1, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2023, 1, 1, 0, 0, 0, TimeSpan.Zero));
Assert.Equal(70d, rollup.Sum(r => r.Cost), 1);
}
// Panel read models compute real figures from the reference data (SDD §8.4–§8.6).
int hausId;
short electricityTypeId;
using (var scope = factory.Services.CreateScope())
{
var services = scope.ServiceProvider;
var wide = new DateOnly(1997, 1, 1);
var toEnd = new DateOnly(2027, 1, 1);
var solar = await services.GetRequiredService().GetSummaryAsync(wide, toEnd);
Assert.True(solar.HasGeneration);
Assert.True(solar.Generation > 0);
// Haus (total_load) + Netz (grid_import) are role-tagged, so self-consumption/savings resolve.
Assert.True(solar.HasLoadContext);
Assert.NotNull(solar.SelfConsumption);
Assert.NotNull(solar.Savings);
var consumables = await services.GetRequiredService().GetConsumablesAsync(wide, toEnd);
var oil = Assert.Single(consumables);
Assert.True(oil.CurrentLevel is > 0);
Assert.NotEmpty(oil.Deliveries);
Assert.True(oil.ConsumptionInRange > 0);
await using var db = fx.CreateContext();
hausId = await db.Meters.Where(m => m.Name == "Zähler Haus").Select(m => m.Id).FirstAsync();
var detail = await services.GetRequiredService().GetAsync(hausId);
Assert.NotNull(detail);
Assert.True(detail!.ReadingCount > 0);
Assert.True(detail.TotalConsumption > 0);
// Flow graph: the demo Haus → Auto chain yields a link + an "Other (Haus)" remainder.
electricityTypeId = await db.EnergyTypes.Where(t => t.Key == "electricity").Select(t => t.Id).FirstAsync();
var flow = await services.GetRequiredService()
.GetFlowAsync(electricityTypeId, new DateOnly(1997, 1, 1), new DateOnly(2027, 1, 1));
Assert.True(flow.HasChain);
Assert.Contains(flow.Nodes, n => n.IsOther);
}
using var client = factory.CreateClient();
var overview = await client.GetAsync(new Uri("/", UriKind.Relative));
overview.EnsureSuccessStatusCode();
var html = await overview.Content.ReadAsStringAsync();
Assert.Contains("Overview", html, StringComparison.Ordinal);
// These labels live only in the rendered-KPI-card branch, so their presence proves the
// summary loaded and the cards rendered (non-ASCII like € is HTML-entity-encoded).
Assert.Contains("This month", html, StringComparison.Ordinal);
Assert.Contains("This year", html, StringComparison.Ordinal);
Assert.Contains("Latest month with data", html, StringComparison.Ordinal);
foreach (var path in new[]
{
"/meters", "/trends", "/solar", "/consumables", "/import",
"/admin/tariffs", "/admin/energy-types", "/admin/categories",
"/admin/connectors", "/admin/settings", $"/meters/{hausId}",
$"/energy/{electricityTypeId}",
})
{
var response = await client.GetAsync(new Uri(path, UriKind.Relative));
response.EnsureSuccessStatusCode();
}
}
finally
{
await using var db = fx.CreateContext();
await ClearDataAsync(db);
}
}
private static async Task ClearDataAsync(MeterVaultDbContext db)
{
await db.MeterLinks.ExecuteDeleteAsync();
await db.Consumption.ExecuteDeleteAsync();
await db.Readings.ExecuteDeleteAsync();
await db.MeterEvents.ExecuteDeleteAsync();
await db.ManualCosts.ExecuteDeleteAsync();
await db.CostCategoryMembers.ExecuteDeleteAsync();
await db.Tariffs.ExecuteDeleteAsync();
await db.Tanks.ExecuteDeleteAsync();
await db.MeterSources.ExecuteDeleteAsync();
await db.Meters.ExecuteDeleteAsync();
await db.ImportBatches.ExecuteDeleteAsync();
}
}