using FluentAssertions; using Marathon.Application.Abstractions; using Marathon.Application.UseCases; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; namespace Marathon.Application.Tests.UseCases; public sealed class ResolvePendingBetsUseCaseTests { private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); private static readonly DateTimeOffset Placed = new(2026, 5, 16, 12, 0, 0, MoscowOffset); private readonly IPlacedBetRepository _bets = Substitute.For(); private readonly IResultRepository _results = Substitute.For(); private ResolvePendingBetsUseCase CreateSut() => new(_bets, _results, NullLogger.Instance); private static PlacedBet MakePending(EventId id, Side side = Side.Side1) => new( Guid.NewGuid(), id, new Bet(MatchScope.Instance, BetType.Win, side, null, new OddsRate(2.10m)), 100m, Placed, BetOutcome.Pending, null); [Fact] public async Task Should_ReturnZero_When_NoPendingBets() { _bets.ListByOutcomeAsync(BetOutcome.Pending, Arg.Any()) .Returns(Array.Empty().ToList().AsReadOnly()); var count = await CreateSut().ExecuteAsync(CancellationToken.None); count.Should().Be(0); await _bets.DidNotReceive().UpdateAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Should_GradeBetsWithResults_AndLeaveOthersAlone() { var idGraded = new EventId("event-1"); var idUngraded = new EventId("event-2"); _bets.ListByOutcomeAsync(BetOutcome.Pending, Arg.Any()) .Returns(new[] { MakePending(idGraded, side: Side.Side1), MakePending(idUngraded, side: Side.Side2), }.ToList().AsReadOnly()); _results.GetAsync(idGraded, Arg.Any()) .Returns(new EventResult(idGraded, 2, 1, Side.Side1, DateTimeOffset.UtcNow)); _results.GetAsync(idUngraded, Arg.Any()) .Returns((EventResult?)null); var count = await CreateSut().ExecuteAsync(CancellationToken.None); count.Should().Be(1, "only event-1 has a result"); await _bets.Received(1).UpdateAsync( Arg.Is(b => b.EventId == idGraded && b.Outcome == BetOutcome.Won), Arg.Any()); await _bets.DidNotReceive().UpdateAsync( Arg.Is(b => b.EventId == idUngraded), Arg.Any()); } [Fact] public async Task Should_CacheResultLookups_PerEvent() { // Two pending bets on the same event — only one result fetch should fire. var id = new EventId("event-shared"); _bets.ListByOutcomeAsync(BetOutcome.Pending, Arg.Any()) .Returns(new[] { MakePending(id, side: Side.Side1), MakePending(id, side: Side.Side2), }.ToList().AsReadOnly()); _results.GetAsync(id, Arg.Any()) .Returns(new EventResult(id, 2, 1, Side.Side1, DateTimeOffset.UtcNow)); await CreateSut().ExecuteAsync(CancellationToken.None); await _results.Received(1).GetAsync(id, Arg.Any()); } }