using FluentAssertions;
using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Pricing.ChargeableChars;
using SIGCM2.Domain.Pricing.ChargeableChars;
namespace SIGCM2.Application.Tests.Pricing.ChargeableChars;
///
/// PRC-001 — ChargeableCharConfigService tests.
/// Covers: per-medio wins over global, global fallback when no per-medio,
/// empty result when no config at all.
///
public class ChargeableCharConfigServiceTests
{
private readonly IChargeableCharConfigRepository _repo = Substitute.For();
private readonly ChargeableCharConfigService _service;
private static readonly DateOnly AsOf = new(2026, 4, 20);
public ChargeableCharConfigServiceTests()
{
_service = new ChargeableCharConfigService(_repo);
}
private static ChargeableCharConfig GlobalConfig(string symbol, decimal price) =>
ChargeableCharConfig.Rehydrate(10L, null, symbol, ChargeableCharCategories.Currency, price, AsOf, null, true);
private static ChargeableCharConfig ProductTypeConfig(long id, int productTypeId, string symbol, decimal price) =>
ChargeableCharConfig.Rehydrate(id, productTypeId, symbol, ChargeableCharCategories.Currency, price, AsOf, null, true);
// ── Global fallback ─────────────────────────────────────────────────────────
[Fact]
public async Task GetActiveConfig_NoPerProductType_ReturnsGlobalConfigs()
{
_repo.GetActiveForProductTypeAsync(1, AsOf, Arg.Any())
.Returns(new List
{
GlobalConfig("$", 1.0m),
GlobalConfig("%", 0.5m),
});
var result = await _service.GetActiveConfigForProductTypeAsync(1, AsOf, CancellationToken.None);
result.Should().ContainKey("$");
result["$"].PricePerUnit.Should().Be(1.0m);
result.Should().ContainKey("%");
}
// ── Per-ProductType wins over global ─────────────────────────────────────────
[Fact]
public async Task GetActiveConfig_PerProductTypeExists_OverridesGlobalForSameSymbol()
{
_repo.GetActiveForProductTypeAsync(5, AsOf, Arg.Any())
.Returns(new List
{
GlobalConfig("$", 1.0m), // global price = 1.0
ProductTypeConfig(20L, 5, "$", 3.0m), // per-PT price = 3.0 → wins
GlobalConfig("%", 0.5m), // global only
});
var result = await _service.GetActiveConfigForProductTypeAsync(5, AsOf, CancellationToken.None);
result["$"].PricePerUnit.Should().Be(3.0m); // per-PT wins
result["%"].PricePerUnit.Should().Be(0.5m); // global only
}
[Fact]
public async Task GetActiveConfig_PerProductTypeExists_IncludesCorrectCategory()
{
_repo.GetActiveForProductTypeAsync(5, AsOf, Arg.Any())
.Returns(new List
{
ProductTypeConfig(20L, 5, "$", 3.0m),
});
var result = await _service.GetActiveConfigForProductTypeAsync(5, AsOf, CancellationToken.None);
result["$"].Category.Should().Be(ChargeableCharCategories.Currency);
}
// ── Empty result ─────────────────────────────────────────────────────────────
[Fact]
public async Task GetActiveConfig_NoConfigAtAll_ReturnsEmptyDictionary()
{
_repo.GetActiveForProductTypeAsync(1, AsOf, Arg.Any())
.Returns(new List());
var result = await _service.GetActiveConfigForProductTypeAsync(1, AsOf, CancellationToken.None);
result.Should().BeEmpty();
}
// ── Key: Symbol, Value: snapshot ─────────────────────────────────────────────
[Fact]
public async Task GetActiveConfig_KeyIsSymbol()
{
_repo.GetActiveForProductTypeAsync(1, AsOf, Arg.Any())
.Returns(new List
{
GlobalConfig("!", 2.0m),
});
var result = await _service.GetActiveConfigForProductTypeAsync(1, AsOf, CancellationToken.None);
result.Should().ContainKey("!");
result["!"].PricePerUnit.Should().Be(2.0m);
}
}