using MeterVault.Core.Analysis.Costing;
using MeterVault.Core.Analysis.Quantities;
using MeterVault.Core.Costing;
using MeterVault.Core.Domain;
using static MeterVault.Core.Tests.Analysis.Costing.CostingTestData;
namespace MeterVault.Core.Tests.Analysis.Costing;
///
/// The tariff book resolves exactly like (meter over type over global, latest start within
/// a scope, both ends inclusive) — but deterministically on ties — and answers the D-35/D-38 questions.
///
public sealed class CostingTariffBookTests
{
private static readonly Tariff[] History =
[
Tariff(1, TariffScope.Global, null, TariffComponent.UnitPrice, 0.30, "EUR/kWh", D(2023, 1, 1)),
Tariff(2, TariffScope.Global, 99, TariffComponent.UnitPrice, 0.31, "EUR/kWh", D(2023, 6, 1)),
TypePrice(3, Electricity, 0.40, "EUR/kWh", D(2023, 3, 1), D(2023, 12, 31)),
TypePrice(4, Electricity, 0.42, "EUR/kWh", D(2023, 9, 1), D(2023, 10, 31)),
MeterPrice(5, Grid, 0.50, "EUR/kWh", D(2023, 5, 1), D(2023, 5, 31)),
TypePrice(6, Water, 5, "EUR/m3", D(2023, 1, 1)),
Tariff(7, TariffScope.EnergyType, Electricity, TariffComponent.BasePrice, 12, "EUR/Monat", D(2023, 1, 1)),
];
public static TheoryData Dates => new()
{
{ Grid, Electricity, 2, 15 },
{ Grid, Electricity, 4, 15 },
{ Grid, Electricity, 5, 15 },
{ Grid, Electricity, 5, 31 },
{ Grid, Electricity, 6, 1 },
{ Grid, Electricity, 9, 15 },
{ Grid, Electricity, 11, 1 },
{ Grid, Electricity, 12, 31 },
{ WaterMeter, Water, 7, 15 },
{ 77, 5, 7, 15 },
};
[Theory]
[MemberData(nameof(Dates))]
public void Resolves_like_the_tariff_resolver(int meterId, int energyTypeId, int month, int day)
{
var book = Book(History);
var date = D(2023, month, day);
foreach (var component in new[] { TariffComponent.UnitPrice, TariffComponent.BasePrice, TariffComponent.FeedIn })
{
var expected = TariffResolver.Resolve(History, component, meterId, energyTypeId, date);
Assert.Same(expected, book.Resolve(component, meterId, energyTypeId, date));
}
}
[Fact]
public void A_newer_price_in_the_same_scope_overrides_an_older_one_that_still_covers_the_date()
{
var book = Book(History);
Assert.Equal(4, book.Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2023, 9, 15))!.Id);
Assert.Equal(3, book.Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2023, 11, 15))!.Id);
Assert.Equal(2, book.Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2024, 1, 15))!.Id);
}
[Fact]
public void A_tie_on_scope_and_start_goes_to_the_later_entry_whatever_the_load_order()
{
var older = TypePrice(10, Electricity, 0.30, "EUR/kWh", D(2025, 1, 1));
var newer = TypePrice(11, Electricity, 0.35, "EUR/kWh", D(2025, 1, 1));
Assert.Same(newer, Book(older, newer).Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2025, 3, 15)));
Assert.Same(newer, Book(newer, older).Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2025, 3, 15)));
}
[Fact]
public void Resolving_in_one_scope_never_falls_back_to_another()
{
var book = Book(History);
Assert.Equal(7, book.ResolveInScope(TariffComponent.BasePrice, TariffScope.EnergyType, Electricity, D(2024, 1, 15))!.Id);
Assert.Null(book.ResolveInScope(TariffComponent.BasePrice, TariffScope.Global, null, D(2024, 1, 15)));
Assert.Null(book.ResolveInScope(TariffComponent.UnitPrice, TariffScope.Meter, Grid, D(2023, 6, 15)));
Assert.Equal(2, book.ResolveInScope(TariffComponent.UnitPrice, TariffScope.Global, 12345, D(2024, 1, 15))!.Id);
}
[Fact]
public void A_scope_is_priced_when_it_has_a_tariff_at_any_date()
{
var book = Book(TypePrice(1, Water, 5, "EUR/m3", D(2030, 1, 1)));
Assert.True(book.HasAny(TariffComponent.UnitPrice, WaterMeter, Water));
Assert.False(book.HasAny(TariffComponent.UnitPrice, Grid, Electricity));
Assert.False(book.HasAny(TariffComponent.FeedIn, WaterMeter, Water));
}
[Fact]
public void A_tariff_that_ends_before_it_starts_is_left_out()
{
var broken = TypePrice(1, Water, 5, "EUR/m3", D(2025, 6, 1), D(2025, 1, 31));
var book = Book(broken);
Assert.Empty(book.Tariffs);
Assert.False(book.HasAny(TariffComponent.UnitPrice, WaterMeter, Water));
}
[Fact]
public void An_own_price_is_known_per_month_by_the_15th()
{
var book = Book(MeterPrice(1, Heater, 0.22, "EUR/kWh", D(2025, 3, 16)), TypePrice(2, Electricity, 0.30, "EUR/kWh", D(2020, 1, 1)));
Assert.True(book.HasMeterScopedUnitPrice(Heater));
Assert.False(book.HasMeterScopedUnitPrice(Grid));
Assert.False(book.HasOwnUnitPrice(Heater, D(2025, 3, 1)));
Assert.True(book.HasOwnUnitPrice(Heater, D(2025, 4, 30)));
Assert.False(book.HasOwnUnitPrice(Grid, D(2025, 4, 1)));
}
[Fact]
public void Units_are_checked_against_the_instance_currency()
{
var eur = TypePrice(1, Electricity, 30, "ct/kWh", D(2025, 1, 1));
var chf = TypePrice(2, Electricity, 0.3, "CHF/kWh", D(2025, 1, 1));
var book = TariffBook.Create([eur, chf], "€");
Assert.Equal(0.3, book.Applicability(eur, "kWh").Convert(30), 12);
Assert.Equal(TariffUnitIssue.CurrencyMismatch, book.Applicability(chf, "kWh").Issue);
Assert.Equal(TariffUnitIssue.IncompatibleUnit, book.Applicability(eur, "m³").Issue);
Assert.Same(book.UnitOf(eur), book.UnitOf(eur));
Assert.Equal(BillingPeriod.Month, book.Accrual(Tariff(3, TariffScope.Global, null, TariffComponent.BasePrice, 5, "EUR/Monat", D(2025, 1, 1))).Period);
}
[Fact]
public void The_price_date_is_the_15th_of_the_month()
{
Assert.Equal(D(2024, 2, 15), TariffBook.PriceDate(D(2024, 2, 29)));
Assert.Equal(D(2025, 12, 15), TariffBook.PriceDate(D(2025, 12, 1)));
}
}