using FluentAssertions; using Marathon.Application.Abstractions; using Marathon.Application.UseCases; using Marathon.Domain.Backtesting; using Marathon.UI.Services; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; namespace Marathon.UI.Tests.Services; /// /// Covers the saved-strategy surface of — chiefly the /// fraction↔percent conversion (domain stores fractions, the form/VM speak percent) /// and the form-validation guard. Run-simulation behaviour is covered elsewhere. /// public sealed class BacktestServiceStrategyTests { private readonly ISavedStrategyRepository _strategies = Substitute.For(); private readonly IAnomalyRepository _anomalies = Substitute.For(); private readonly IEventRepository _events = Substitute.For(); private readonly IResultRepository _results = Substitute.For(); private BacktestService CreateSut() { var run = new RunBacktestUseCase(_anomalies, _events, _results, NullLogger.Instance); var save = new SaveStrategyUseCase(_strategies, NullLogger.Instance); var delete = new DeleteStrategyUseCase(_strategies, NullLogger.Instance); return new BacktestService(run, save, delete, _strategies); } [Fact] public async Task ListStrategiesAsync_ConvertsStoredFractionsToPercent() { var preset = new SavedStrategy( Guid.NewGuid(), "Quarter Kelly", new BacktestStrategy(1000m, 0.45m, StakeRule.Kelly, 50m, PercentOfBankroll: 0.03m, KellyFraction: 0.25m), DateTimeOffset.UtcNow); _strategies.ListAsync(Arg.Any()).Returns(new[] { preset }); var vms = await CreateSut().ListStrategiesAsync(CancellationToken.None); vms.Should().ContainSingle(); vms[0].Name.Should().Be("Quarter Kelly"); vms[0].StakeRule.Should().Be(StakeRule.Kelly); vms[0].PercentOfBankrollPercent.Should().Be(3m); // 0.03 fraction → 3% vms[0].KellyFractionPercent.Should().Be(25m); // 0.25 fraction → 25% } [Fact] public async Task SaveStrategyAsync_PersistsFormPercents_AsFractions() { _strategies.GetByNameAsync(Arg.Any(), Arg.Any()).Returns((SavedStrategy?)null); var form = new BacktestForm { StartingBankroll = 1000m, MinScore = 0.5m, StakeRule = StakeRule.PercentOfBankroll, FlatStake = 50m, PercentOfBankrollPercent = 4m, KellyFractionPercent = 25m, }; var vm = await CreateSut().SaveStrategyAsync("PoB", form, CancellationToken.None); vm.PercentOfBankrollPercent.Should().Be(4m); await _strategies.Received(1).AddAsync( Arg.Is(s => s.Name == "PoB" && s.Strategy.PercentOfBankroll == 0.04m), Arg.Any()); } [Fact] public async Task SaveStrategyAsync_Throws_And_DoesNotPersist_When_FormInvalid() { var badForm = new BacktestForm { StartingBankroll = 0m }; // fails IsValid var act = async () => await CreateSut().SaveStrategyAsync("X", badForm, CancellationToken.None); await act.Should().ThrowAsync(); await _strategies.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task DeleteStrategyAsync_DelegatesToRepository() { var id = Guid.NewGuid(); await CreateSut().DeleteStrategyAsync(id, CancellationToken.None); await _strategies.Received(1).DeleteAsync(id, Arg.Any()); } }