using System;
using System.Collections.Generic;
using System.Linq;
using PolyTrader.Modules.Accounting.Models;
using PolyTrader.Modules.Accounting.Persistence;
using PolyTrader.Modules.Accounting.Services;
using Xunit;
namespace PolyTrader.Tests.Accounting
{
///
/// Die Warnung über veraltete EZB-Kurse (#533). Die Schwelle liegt bei drei Tagen, weil genau so
/// groß die planmäßige Wochenendlücke ist: Der Freitagskurs gilt bis Montag und ist dabei kein
/// Mangel. Erst darüber hinaus fehlt wirklich etwas.
///
public class CurrencyStalenessTests
{
private sealed class StubFx : IFxRateRepository
{
private readonly List _rates;
public StubFx(params FxRate[] rates) => _rates = rates.ToList();
public void Upsert(FxRate rate) => _rates.Add(rate);
public List GetAll() => _rates.OrderBy(r => r.Date).ToList();
}
private sealed class ThrowingFx : IFxRateRepository
{
public void Upsert(FxRate rate) => throw new InvalidOperationException("DB weg");
public List GetAll() => throw new InvalidOperationException("DB weg");
}
/// Leeres Ledger: Hier zählt allein die Währungsauflösung, nicht die Aggregation.
private sealed class EmptyLedger : ILedgerRepository
{
public bool Upsert(LedgerEntry entry) => false;
public DateTime? LatestTimestamp(int accountId) => null;
public decimal SumNet(int accountId) => 0m;
public int Count(int accountId) => 0;
public List Query(int? accountId, DateTime? from, DateTime? to, int limit) => new();
public List GetUpTo(int? accountId, DateTime to) => new();
}
private static AccountingReportService Build(IFxRateRepository fx) => new(new EmptyLedger(), fx);
private static FxRate Rate(int year, int month, int day, decimal usdToEur = 0.86m) =>
new() { Date = new DateTime(year, month, day), UsdToEur = usdToEur, Source = "ECB" };
[Fact]
public void Frisch_gerechneter_Kurs_warnt_nicht()
{
var ctx = Build(new StubFx(Rate(2026, 9, 9))).ResolveCurrency("EUR", new DateTime(2026, 9, 10));
Assert.True(ctx.Available);
Assert.False(ctx.IsStale);
Assert.Null(ctx.Warning);
Assert.Equal(1, ctx.RateAgeDays);
Assert.Equal(new DateTime(2026, 9, 9), ctx.RateDate);
}
[Fact]
public void Wochenende_gilt_noch_nicht_als_veraltet()
{
// Freitag 2026-09-04 → Montag 2026-09-07: drei Tage, der Normalfall.
var ctx = Build(new StubFx(Rate(2026, 9, 4))).ResolveCurrency("EUR", new DateTime(2026, 9, 7));
Assert.Equal(3, ctx.RateAgeDays);
Assert.False(ctx.IsStale);
}
[Fact]
public void Aelter_als_drei_Tage_warnt()
{
var ctx = Build(new StubFx(Rate(2026, 9, 4))).ResolveCurrency("EUR", new DateTime(2026, 9, 8));
Assert.True(ctx.Available); // benutzbar bleibt er
Assert.True(ctx.IsStale);
Assert.Equal(4, ctx.RateAgeDays);
Assert.Contains("04.09.2026", ctx.Warning);
Assert.Contains("4 Tage", ctx.Warning);
}
[Fact]
public void Ohne_Kurse_ist_EUR_nicht_verfuegbar()
{
var ctx = Build(new StubFx()).ResolveCurrency("EUR", new DateTime(2026, 9, 10));
Assert.False(ctx.Available);
Assert.Equal(0m, ctx.Factor);
Assert.Contains("Keine EZB-Tageskurse", ctx.Note);
}
[Fact]
public void Rueckwirkende_Abrechnung_nimmt_den_Kurs_von_damals()
{
var fx = new StubFx(Rate(2026, 3, 31, 0.92m), Rate(2026, 9, 9, 0.86m));
var maerz = Build(fx).ResolveCurrency("EUR", new DateTime(2026, 3, 31));
Assert.Equal(0.92m, maerz.Factor); // nicht der heutige Kurs
Assert.False(maerz.IsStale); // am Stichtag war er taggenau
}
[Fact]
public void Datenbankausfall_sperrt_EUR_statt_zu_werfen()
{
var ctx = Build(new ThrowingFx()).ResolveCurrency("EUR", new DateTime(2026, 9, 10));
Assert.False(ctx.Available);
Assert.Equal(0m, ctx.Factor);
}
[Fact]
public void USDC_und_USD_bleiben_ohne_Kurse_verfuegbar()
{
var svc = Build(new StubFx());
var usdc = svc.ResolveCurrency("USDC", new DateTime(2026, 9, 10));
var usd = svc.ResolveCurrency("USD", new DateTime(2026, 9, 10));
Assert.True(usdc.Available);
Assert.Equal(1.0m, usdc.Factor);
Assert.True(usd.Available);
Assert.False(usd.IsStale);
}
}
}