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 UpdatePlacedBetUseCaseTests { private static readonly TimeSpan Msk = TimeSpan.FromHours(3); private static readonly DateTimeOffset PlacedAt = new(2026, 5, 16, 12, 0, 0, Msk); private readonly IPlacedBetRepository _bets = Substitute.For(); private readonly IEventRepository _events = Substitute.For(); private readonly IResultRepository _results = Substitute.For(); private UpdatePlacedBetUseCase CreateSut() => new(_bets, _events, _results, NullLogger.Instance); private static Bet Selection(Side side = Side.Side1, decimal rate = 2.0m) => new(MatchScope.Instance, BetType.Win, side, null, new OddsRate(rate)); private static PlacedBet Existing(Guid id, EventId eventId) => new(id, eventId, Selection(rate: 1.5m), 50m, PlacedAt, BetOutcome.Pending, "old note"); private static Event Ev(EventId id) => new(id, new SportCode(11), "England", "L", "C", PlacedAt.AddDays(1), "Home", "Away"); [Fact] public async Task Throws_When_BetUnknown() { var id = Guid.NewGuid(); _bets.GetAsync(id, Arg.Any()).Returns((PlacedBet?)null); var act = async () => await CreateSut().ExecuteAsync(id, new EventId("e"), Selection(), 100m, null); await act.Should().ThrowAsync().WithMessage("*unknown bet*"); await _bets.DidNotReceive().UpdateAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Throws_When_EventUnknown() { var id = Guid.NewGuid(); var eid = new EventId("missing"); _bets.GetAsync(id, Arg.Any()).Returns(Existing(id, eid)); _events.GetAsync(eid, Arg.Any()).Returns((Event?)null); var act = async () => await CreateSut().ExecuteAsync(id, eid, Selection(), 100m, null); await act.Should().ThrowAsync().WithMessage("*unknown event*"); await _bets.DidNotReceive().UpdateAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Updates_PreservesPlacedAt_AppliesChanges_AndRegrades() { var id = Guid.NewGuid(); var eid = new EventId("evt-1"); _bets.GetAsync(id, Arg.Any()).Returns(Existing(id, eid)); _events.GetAsync(eid, Arg.Any()).Returns(Ev(eid)); // Side1 wins → the (new) Side1 selection re-grades to Won. _results.GetAsync(eid, Arg.Any()) .Returns(new EventResult(eid, 2, 0, Side.Side1, PlacedAt.AddHours(3))); var updated = await CreateSut().ExecuteAsync( id, eid, Selection(side: Side.Side1, rate: 2.5m), stake: 120m, notes: "new note"); updated.Id.Should().Be(id); updated.PlacedAt.Should().Be(PlacedAt); // original entry time preserved updated.Stake.Should().Be(120m); updated.Selection.Rate.Value.Should().Be(2.5m); updated.Notes.Should().Be("new note"); updated.Outcome.Should().Be(BetOutcome.Won); // re-graded against the result await _bets.Received(1).UpdateAsync( Arg.Is(b => b.Id == id && b.Outcome == BetOutcome.Won && b.Stake == 120m), Arg.Any()); await _bets.Received(1).SaveChangesAsync(Arg.Any()); } [Fact] public async Task NotesOnlyEdit_PreservesSettledOutcome_EvenWhenResultPruned() { var id = Guid.NewGuid(); var eid = new EventId("evt-1"); var sameSelection = Selection(side: Side.Side1, rate: 2.0m); var settledWon = new PlacedBet(id, eid, sameSelection, 50m, PlacedAt, BetOutcome.Won, "old"); _bets.GetAsync(id, Arg.Any()).Returns(settledWon); _events.GetAsync(eid, Arg.Any()).Returns(Ev(eid)); // Result row has been pruned by retention. _results.GetAsync(eid, Arg.Any()).Returns((EventResult?)null); // Same selection/event — only the note changes. var updated = await CreateSut().ExecuteAsync(id, eid, sameSelection, stake: 50m, notes: "new note"); updated.Outcome.Should().Be(BetOutcome.Won); // NOT silently reset to Pending updated.Notes.Should().Be("new note"); } [Fact] public async Task ChangedSelection_WithNoResult_ResetsToPending() { var id = Guid.NewGuid(); var eid = new EventId("evt-1"); var settledWon = new PlacedBet(id, eid, Selection(side: Side.Side1, rate: 2.0m), 50m, PlacedAt, BetOutcome.Won, "old"); _bets.GetAsync(id, Arg.Any()).Returns(settledWon); _events.GetAsync(eid, Arg.Any()).Returns(Ev(eid)); _results.GetAsync(eid, Arg.Any()).Returns((EventResult?)null); // Selection changed (Side1 → Side2) but no result to grade against → must be Pending, // never the now-stale Won. var updated = await CreateSut().ExecuteAsync(id, eid, Selection(side: Side.Side2, rate: 3.0m), stake: 50m, notes: "old"); updated.Outcome.Should().Be(BetOutcome.Pending); } }