using FluentAssertions; using Marathon.Application.Storage; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; using Marathon.Infrastructure.Persistence; using Marathon.Infrastructure.Persistence.Repositories; namespace Marathon.Infrastructure.Tests.Persistence; /// /// Round-trip + query tests for . Uses the /// in-memory SQLite fixture so the schema + indices declared in the migration /// are exercised on every test. /// public sealed class PlacedBetRoundTripTests : IDisposable { private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); private static readonly DateTimeOffset Placed = new(2026, 5, 16, 12, 0, 0, MoscowOffset); private readonly InMemoryDbFixture _fixture; private readonly PlacedBetRepository _repo; public PlacedBetRoundTripTests() { _fixture = new InMemoryDbFixture(); _repo = new PlacedBetRepository(_fixture.DbContext); } public void Dispose() => _fixture.Dispose(); private static PlacedBet MakeBet( Guid? id = null, string eventCode = "12345678", BetType type = BetType.Win, Side side = Side.Side1, decimal? value = null, decimal rate = 2.10m, decimal stake = 100m, DateTimeOffset? placedAt = null, BetOutcome outcome = BetOutcome.Pending, string? notes = null) => new( Id: id ?? Guid.NewGuid(), EventId: new EventId(eventCode), Selection: new Bet(MatchScope.Instance, type, side, value is { } v ? new OddsValue(v) : null, new OddsRate(rate)), Stake: stake, PlacedAt: placedAt ?? Placed, Outcome: outcome, Notes: notes); [Fact] public async Task PlacedBet_RoundTrip_PreservesAllFields() { var bet = MakeBet( type: BetType.WinFora, side: Side.Side2, value: -1.5m, rate: 2.30m, stake: 250m, notes: "test note"); await _repo.AddAsync(bet); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var retrieved = await _repo.GetAsync(bet.Id); retrieved.Should().NotBeNull(); retrieved!.Id.Should().Be(bet.Id); retrieved.EventId.Value.Should().Be("12345678"); retrieved.Selection.Type.Should().Be(BetType.WinFora); retrieved.Selection.Side.Should().Be(Side.Side2); retrieved.Selection.Value!.Value.Should().Be(-1.5m); retrieved.Selection.Rate.Value.Should().Be(2.30m); retrieved.Stake.Should().Be(250m); retrieved.PlacedAt.Should().Be(Placed); retrieved.PlacedAt.Offset.Should().Be(MoscowOffset); retrieved.Outcome.Should().Be(BetOutcome.Pending); retrieved.Notes.Should().Be("test note"); } [Fact] public async Task ListByOutcomeAsync_ReturnsOnlyMatching() { await _repo.AddAsync(MakeBet(outcome: BetOutcome.Pending, eventCode: "1")); await _repo.AddAsync(MakeBet(outcome: BetOutcome.Won, eventCode: "2")); await _repo.AddAsync(MakeBet(outcome: BetOutcome.Pending, eventCode: "3")); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var pending = await _repo.ListByOutcomeAsync(BetOutcome.Pending); pending.Should().HaveCount(2); pending.Should().OnlyContain(b => b.Outcome == BetOutcome.Pending); } [Fact] public async Task ListByEventAsync_ReturnsEveryBet_OnTheEvent() { await _repo.AddAsync(MakeBet(eventCode: "evt-A", side: Side.Side1)); await _repo.AddAsync(MakeBet(eventCode: "evt-A", side: Side.Side2)); await _repo.AddAsync(MakeBet(eventCode: "evt-B")); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var onA = await _repo.ListByEventAsync(new EventId("evt-A")); onA.Should().HaveCount(2); onA.Should().OnlyContain(b => b.EventId.Value == "evt-A"); } [Fact] public async Task ListByDateRangeAsync_FiltersByPlacedAt() { var older = new DateTimeOffset(2026, 5, 1, 12, 0, 0, MoscowOffset); var newer = new DateTimeOffset(2026, 5, 20, 12, 0, 0, MoscowOffset); await _repo.AddAsync(MakeBet(placedAt: older, eventCode: "old")); await _repo.AddAsync(MakeBet(placedAt: newer, eventCode: "new")); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var range = new DateRange( from: new DateTimeOffset(2026, 5, 10, 0, 0, 0, MoscowOffset), to: new DateTimeOffset(2026, 5, 31, 0, 0, 0, MoscowOffset)); var inRange = await _repo.ListByDateRangeAsync(range); inRange.Should().HaveCount(1); inRange[0].EventId.Value.Should().Be("new"); } [Fact] public async Task UpdateAsync_PersistsOutcomeChange() { var bet = MakeBet(outcome: BetOutcome.Pending); await _repo.AddAsync(bet); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var graded = bet.WithOutcome(BetOutcome.Won); await _repo.UpdateAsync(graded); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var reloaded = await _repo.GetAsync(bet.Id); reloaded!.Outcome.Should().Be(BetOutcome.Won); } [Fact] public async Task PlacedBet_Survives_Event_Deletion() { // Documents the explicit "no foreign key" design choice — the journal // is user data and must survive snapshot retention pruning the source // event row. var eventRepo = new EventRepository(_fixture.DbContext); var evt = new Event( Id: new EventId("evt-prune"), Sport: new SportCode(11), CountryCode: "England", LeagueId: "league", Category: string.Empty, ScheduledAt: new DateTimeOffset(2026, 5, 16, 20, 0, 0, MoscowOffset), Side1Name: "Home", Side2Name: "Away"); await eventRepo.AddAsync(evt); await eventRepo.SaveChangesAsync(); var bet = MakeBet(eventCode: "evt-prune"); await _repo.AddAsync(bet); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); // Delete the event — the bet should remain untouched. await eventRepo.DeleteAsync(new EventId("evt-prune")); await eventRepo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var stillThere = await _repo.GetAsync(bet.Id); stillThere.Should().NotBeNull(); stillThere!.EventId.Value.Should().Be("evt-prune"); } [Fact] public async Task DeleteAsync_RemovesBet() { var bet = MakeBet(); await _repo.AddAsync(bet); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); await _repo.DeleteAsync(bet.Id); await _repo.SaveChangesAsync(); (await _repo.GetAsync(bet.Id)).Should().BeNull(); } }