using Xunit;
using KArtSell.BuildingBlocks.Time;
using KArtSell.Modules.ModelOperations.ShadowRun;
using Microsoft.Extensions.Logging;
namespace KArtSell.Integration.Tests;
///
/// Pure unit tests for Shadow Run components.
/// NO external API calls, NO database, NO I/O.
/// Uses strict mocks/stubs.
///
public sealed class ShadowRunUnitTests
{
private readonly ILogger _backfillerLogger = new NoOpLogger();
private readonly ILogger _replayLogger = new NoOpLogger();
private readonly ILogger _calculatorLogger = new NoOpLogger();
///
/// DataBackfiller should generate OHLCV bars for all trading days.
///
[Fact]
public async Task DataBackfiller_GeneratesOhlcvBars_ForAllTradingDays()
{
// Arrange
var calendar = new StubMarketCalendar();
var krxData = new StubKrxData();
var backfiller = new DataBackfiller(calendar, krxData, _backfillerLogger);
// Act: 30-day window
var bars = await backfiller.BackfillOhlcvAsync(
new DateOnly(2026, 4, 1),
new DateOnly(2026, 4, 30),
new[] { "KOSPI", "KOSDAQ" }.ToList(),
CancellationToken.None);
// Assert
Assert.NotEmpty(bars);
Assert.True(bars.Count >= 20, $"Expected 20+ bars (trading days), got {bars.Count}");
// Verify both tickers present
var tickers = bars.Select(b => b.Ticker).Distinct().ToList();
Assert.Contains("KOSPI", tickers);
Assert.Contains("KOSDAQ", tickers);
}
///
/// ReplayEngine should handle zero-order scenario gracefully.
///
[Fact]
public async Task ReplayEngine_HandlesZeroOrders_WithoutCrash()
{
// Arrange
var replay = new ReplayEngine(_replayLogger);
var bars = new List
{
new(new DateOnly(2026, 4, 1), "KOSPI", 2500, 2510, 2490, 2505, 1_000_000),
new(new DateOnly(2026, 4, 2), "KOSPI", 2505, 2515, 2495, 2510, 1_000_000),
};
var fees = new List
{
new(new DateOnly(2026, 4, 1), 0.001m, 0.0005m),
};
var sessions = new[] { new DateOnly(2026, 4, 1), new DateOnly(2026, 4, 2) }.ToList();
// Act
var result = await replay.ReplayAsync(
Guid.NewGuid(), bars, fees, 10_000_000m, sessions, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result.PortfolioHistory);
Assert.Equal(sessions.Count, result.PortfolioHistory.Count);
}
///
/// DataBackfiller should validate completeness.
///
[Fact]
public async Task DataBackfiller_ValidatesCompleteness()
{
// Arrange
var calendar = new StubMarketCalendar();
var krxData = new StubKrxData();
var backfiller = new DataBackfiller(calendar, krxData, _backfillerLogger);
var bars = new List
{
new(new DateOnly(2026, 4, 1), "KOSPI", 2500, 2510, 2490, 2505, 1_000_000),
// Missing KOSDAQ on 2026-04-01
};
var fees = new List
{
new(new DateOnly(2026, 4, 1), 0.001m, 0.0005m),
};
// Act
var result = await backfiller.ValidateAsync(
bars, fees,
new[] { "KOSPI", "KOSDAQ" }.ToList(),
new DateOnly(2026, 4, 1),
new DateOnly(2026, 4, 2),
CancellationToken.None);
// Assert
Assert.True(result.HasIssues);
Assert.NotEmpty(result.MissingTickers ?? new List());
}
// Stub implementations (no real I/O)
private sealed class StubMarketCalendar : IMarketCalendarService
{
public Task> GetTradingSessionsAsync(
DateOnly start, DateOnly end, CancellationToken ct)
{
var sessions = new List();
for (var d = start; d <= end; d = d.AddDays(1))
{
if (d.DayOfWeek != DayOfWeek.Saturday && d.DayOfWeek != DayOfWeek.Sunday)
sessions.Add(d);
}
return Task.FromResult>(sessions.AsReadOnly());
}
}
private sealed class StubKrxData : IKrxDataService
{
public Task> GetDailyOhlcvAsync(
string ticker, DateOnly start, DateOnly endDate, CancellationToken ct)
{
var bars = new List();
for (var d = start; d <= endDate; d = d.AddDays(1))
{
if (d.DayOfWeek != DayOfWeek.Saturday && d.DayOfWeek != DayOfWeek.Sunday)
bars.Add(new DataBackfiller.OhlcvBar(
d, ticker, 2500, 2510, 2490, 2505, 1_000_000));
}
return Task.FromResult>(bars.AsReadOnly());
}
public Task> GetFeeScheduleAsync(
DateOnly start, DateOnly endDate, CancellationToken ct)
{
return Task.FromResult>(
new[] { new DataBackfiller.FeeScheduleEntry(start, 0.001m, 0.0005m) }
.ToList().AsReadOnly());
}
}
private sealed class NoOpLogger : ILogger
{
public IDisposable? BeginScope(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => false;
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func formatter) { }
}
}