using FluentAssertions; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; using Marathon.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; namespace Marathon.Infrastructure.Tests.Persistence; /// /// Round-trip + query tests for . Uses the in-memory /// SQLite fixture so the table + unique AnomalyId / Outcome indexes are exercised. /// public sealed class PaperBetRoundTripTests : IDisposable { private static readonly TimeSpan Msk = TimeSpan.FromHours(3); private static readonly DateTimeOffset Opened = new(2026, 5, 20, 18, 0, 0, Msk); private readonly InMemoryDbFixture _fixture; private readonly PaperBetRepository _repo; public PaperBetRoundTripTests() { _fixture = new InMemoryDbFixture(); _repo = new PaperBetRepository(_fixture.DbContext); } public void Dispose() => _fixture.Dispose(); private static PaperBet Open( Guid? anomalyId = null, string eventCode = "26000001", Side pick = Side.Side1) => PaperBet.Open(anomalyId ?? Guid.NewGuid(), new EventId(eventCode), pick, 1.95m, 10m, Opened); [Fact] public async Task RoundTrip_PreservesAllFields_ForSettledBet() { var settled = Open(pick: Side.Side2).SettleAgainst(Side.Side2, Opened.AddHours(2)); await _repo.AddAsync(settled); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var got = await _repo.GetAsync(settled.Id); got.Should().NotBeNull(); got!.Id.Should().Be(settled.Id); got.AnomalyId.Should().Be(settled.AnomalyId); got.EventId.Value.Should().Be("26000001"); got.PickedSide.Should().Be(Side.Side2); got.Rate.Should().Be(1.95m); got.Stake.Should().Be(10m); got.OpenedAt.Should().Be(Opened); got.OpenedAt.Offset.Should().Be(Msk); got.Outcome.Should().Be(BetOutcome.Won); got.Payout.Should().Be(19.5m); got.SettledAt.Should().Be(Opened.AddHours(2)); } [Fact] public async Task RoundTrip_OpenBet_HasNullSettlementFields() { var open = Open(); await _repo.AddAsync(open); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var got = await _repo.GetAsync(open.Id); got!.Outcome.Should().Be(BetOutcome.Pending); got.SettledAt.Should().BeNull(); got.Payout.Should().BeNull(); } [Fact] public async Task ListByOutcomeAsync_ReturnsOnlyMatching() { await _repo.AddAsync(Open(eventCode: "open1")); await _repo.AddAsync(Open(eventCode: "open2")); await _repo.AddAsync(Open(eventCode: "won1").SettleAgainst(Side.Side1, Opened.AddHours(1))); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var open = await _repo.ListByOutcomeAsync(BetOutcome.Pending); open.Should().HaveCount(2); open.Should().OnlyContain(b => b.Outcome == BetOutcome.Pending); } [Fact] public async Task GetExistingAnomalyIdsAsync_ReturnsKnownSubset() { var known = Guid.NewGuid(); await _repo.AddAsync(Open(anomalyId: known)); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var unknown = Guid.NewGuid(); var existing = await _repo.GetExistingAnomalyIdsAsync(new[] { known, unknown }); existing.Should().ContainSingle().And.Contain(known); existing.Should().NotContain(unknown); } [Fact] public async Task GetExistingAnomalyIdsAsync_EmptyInput_ReturnsEmpty_WithoutQuery() { (await _repo.GetExistingAnomalyIdsAsync(Array.Empty())).Should().BeEmpty(); } [Fact] public async Task UpdateAsync_PersistsSettlement() { var bet = Open(pick: Side.Side1); await _repo.AddAsync(bet); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); await _repo.UpdateAsync(bet.SettleAgainst(Side.Side1, Opened.AddHours(3))); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); var got = await _repo.GetAsync(bet.Id); got!.Outcome.Should().Be(BetOutcome.Won); got.Payout.Should().Be(19.5m); } [Fact] public async Task UniqueAnomalyIdIndex_RejectsSecondBet_ForSameAnomaly() { var anomalyId = Guid.NewGuid(); await _repo.AddAsync(Open(anomalyId: anomalyId, eventCode: "a")); await _repo.SaveChangesAsync(); _fixture.DbContext.ChangeTracker.Clear(); await _repo.AddAsync(Open(anomalyId: anomalyId, eventCode: "b")); var act = async () => await _repo.SaveChangesAsync(); await act.Should().ThrowAsync(); } }