using System.Globalization;
using MeterVault.Core.Analysis;
using MeterVault.Core.Analysis.Costing;
using MeterVault.Core.Analysis.Coverage;
using MeterVault.Core.Analysis.Totals;
using MeterVault.Core.Domain;
using MeterVault.Infrastructure.Analysis;
namespace MeterVault.Integration.Tests.Analysis;
///
/// Builders for the pure tests of the shared analysis components: real buckets from the period resolver and the bucket
/// planner (Berlin, frozen clock), values, reader series, priced cost figures, and a culture scope — the machine running
/// the tests may be German, and the components format for the reader.
///
internal static class AnalysisUiTestData
{
public static readonly TimeZoneInfo Berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
/// Long after every range the tests use, so custom ranges resolve as complete.
public static readonly DateTimeOffset FarFuture = new(2040, 1, 1, 0, 0, 0, TimeSpan.Zero);
/// 19 September 2026, 14:37 Berlin.
public static readonly DateTimeOffset Now = new DateTimeOffset(2026, 9, 19, 14, 37, 0, TimeSpan.FromHours(2)).ToUniversalTime();
public static DateOnly D(int year, int month, int day) => new(year, month, day);
public static ResolvedPeriod Range(DateOnly first, DateOnly last) =>
PeriodResolver.Resolve(PeriodPreset.Custom, first, last, FarFuture, Berlin);
public static IReadOnlyList Buckets(DateOnly first, DateOnly last, BucketSize size = BucketSize.Month) =>
BucketPlanner.Plan(Range(first, last), size).Buckets;
public static BucketValue Available(double value, Provenance provenance = Provenance.Measured) => BucketValue.Available(value, provenance);
public static BucketValue Partial(double value) =>
new(value, BucketStatus.Partial, Provenance.Measured, ValueIssue.PartialCoverage);
public static BucketValue Missing() => BucketValue.Missing();
/// A physical meter's series in kWh with its total and, optionally, a comparison.
public static AnalysisSeries Series(
int meterId,
string name,
IReadOnlyList values,
BucketValue? total = null,
QuantityKind kind = QuantityKind.Consumption,
SeriesComparison? comparison = null) =>
new(SeriesKey.ForMeter(meterId, 1, "kWh"), name, SeriesBasis.Physical, kind, "kWh", values, total ?? Available(values.Sum(v => v.Value ?? 0)), IsAdditive: true)
{
Comparison = comparison,
};
public static SeriesComparison Comparison(IReadOnlyList values, BucketValue total, Change change) =>
new(values, total, MatchedCoverageResult.NotComparable, null, null, change);
///
/// One energy type's bill line priced over at €/kWh from
/// (no tariff at all when null), with kWh per month.
///
public static CostResult Priced(IReadOnlyList buckets, double amount, double? price, DateOnly? priceFrom = null)
{
Tariff[] tariffs = price is { } value
? [new Tariff
{
Id = 1,
ScopeType = TariffScope.EnergyType,
ScopeId = 1,
Component = TariffComponent.UnitPrice,
Value = value,
Unit = "EUR/kWh",
ValidFrom = priceFrom ?? D(2000, 1, 1),
}]
: [];
var quantities = CostCalculator.Parts(buckets).Select(p => CostQuantity.Known(p, amount)).ToList();
var line = new CostLine(10, 1, BillLineKind.UnitPrice, "kWh", quantities);
return CostCalculator.Calculate(new CostRequest(buckets, D(2039, 12, 31), TariffBook.Create(tariffs, "EUR"), [line]));
}
/// Runs with as the formatting and the UI culture.
public static T In(string culture, Func body)
{
var format = CultureInfo.CurrentCulture;
var ui = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(culture);
return body();
}
finally
{
CultureInfo.CurrentCulture = format;
CultureInfo.CurrentUICulture = ui;
}
}
public static void In(string culture, Action body) => In(culture, () =>
{
body();
return 0;
});
}