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