using FluentAssertions; using Marathon.Domain.Betting; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; namespace Marathon.Domain.Tests.Betting; /// /// Unit tests for across every bet type + /// every important boundary (handicap push, total push, period-scope null). /// public sealed class BetOutcomeResolverTests { private static readonly EventId EventId = new("12345678"); // ── Win bets ───────────────────────────────────────────────────────────── [Theory] [InlineData(Side.Side1, Side.Side1, BetOutcome.Won)] [InlineData(Side.Side1, Side.Side2, BetOutcome.Lost)] [InlineData(Side.Side1, Side.Draw, BetOutcome.Lost)] [InlineData(Side.Side2, Side.Side2, BetOutcome.Won)] [InlineData(Side.Side2, Side.Side1, BetOutcome.Lost)] public void Should_GradeWinBet(Side selectionSide, Side winner, BetOutcome expected) { var bet = MakeBet(BetType.Win, selectionSide); var result = MakeResult(winner); BetOutcomeResolver.Resolve(bet, result).Should().Be(expected); } // ── Draw bets ──────────────────────────────────────────────────────────── [Theory] [InlineData(Side.Draw, BetOutcome.Won)] [InlineData(Side.Side1, BetOutcome.Lost)] [InlineData(Side.Side2, BetOutcome.Lost)] public void Should_GradeDrawBet(Side winner, BetOutcome expected) { var bet = MakeBet(BetType.Draw, Side.Draw); var result = MakeResult(winner, 1, 1); BetOutcomeResolver.Resolve(bet, result).Should().Be(expected); } // ── Handicap (WinFora) ─────────────────────────────────────────────────── [Theory] // Side1 with +1.5 handicap; final 0-2 → adjusted 1.5 vs 2 → loss [InlineData(Side.Side1, 1.5, 0, 2, BetOutcome.Lost)] // Side1 with +1.5; final 1-2 → adjusted 2.5 vs 2 → win [InlineData(Side.Side1, 1.5, 1, 2, BetOutcome.Won)] // Side1 with -1.5; final 3-1 → adjusted 1.5 vs 1 → win [InlineData(Side.Side1, -1.5, 3, 1, BetOutcome.Won)] // Whole-number handicap that ties: Side1 +1, final 1-2 → 2 vs 2 → push (Void) [InlineData(Side.Side1, 1, 1, 2, BetOutcome.Void)] // Side2 with -1; final 0-2 → adjusted Side2 1 vs Side1 0 → win [InlineData(Side.Side2, -1, 0, 2, BetOutcome.Won)] public void Should_GradeHandicapBet( Side side, double handicap, int s1, int s2, BetOutcome expected) { var bet = new Bet(MatchScope.Instance, BetType.WinFora, side, new OddsValue((decimal)handicap), new OddsRate(1.85m)); var result = new EventResult(EventId, s1, s2, DeriveWinner(s1, s2), DateTimeOffset.UtcNow); BetOutcomeResolver.Resolve(bet, result).Should().Be(expected); } // ── Totals ─────────────────────────────────────────────────────────────── [Theory] // Over 2.5; total 3 → win [InlineData(Side.More, 2.5, 1, 2, BetOutcome.Won)] // Over 2.5; total 2 → loss [InlineData(Side.More, 2.5, 0, 2, BetOutcome.Lost)] // Over 3.0; total 3 → push (Void) [InlineData(Side.More, 3.0, 1, 2, BetOutcome.Void)] // Under 2.5; total 2 → win [InlineData(Side.Less, 2.5, 1, 1, BetOutcome.Won)] // Under 2.5; total 3 → loss [InlineData(Side.Less, 2.5, 1, 2, BetOutcome.Lost)] public void Should_GradeTotalBet( Side side, double threshold, int s1, int s2, BetOutcome expected) { var bet = new Bet(MatchScope.Instance, BetType.Total, side, new OddsValue((decimal)threshold), new OddsRate(1.85m)); var result = new EventResult(EventId, s1, s2, DeriveWinner(s1, s2), DateTimeOffset.UtcNow); BetOutcomeResolver.Resolve(bet, result).Should().Be(expected); } // ── Period-scope guard ─────────────────────────────────────────────────── [Fact] public void Should_ReturnNull_When_BetScopeIsPeriod() { var bet = new Bet(new PeriodScope(1), BetType.Win, Side.Side1, value: null, new OddsRate(2.10m)); var result = MakeResult(Side.Side1); BetOutcomeResolver.Resolve(bet, result).Should().BeNull( "period scope cannot be graded from full-time score alone"); } [Fact] public void Should_Throw_When_BetOrResultIsNull() { ((Action)(() => BetOutcomeResolver.Resolve(null!, MakeResult(Side.Side1)))) .Should().Throw(); ((Action)(() => BetOutcomeResolver.Resolve(MakeBet(BetType.Win, Side.Side1), null!))) .Should().Throw(); } // ── Helpers ────────────────────────────────────────────────────────────── private static Bet MakeBet(BetType type, Side side) => new(MatchScope.Instance, type, side, value: null, new OddsRate(2.00m)); private static EventResult MakeResult(Side winner, int s1 = 1, int s2 = 0) => new(EventId, s1, s2, winner, DateTimeOffset.UtcNow); private static Side DeriveWinner(int s1, int s2) => s1 == s2 ? Side.Draw : (s1 > s2 ? Side.Side1 : Side.Side2); }