using FluentAssertions; using Marathon.Application.Betting; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; namespace Marathon.Application.Tests.Betting; /// /// Unit tests for covering the math /// itself and the snapshot-matching path used by the report use case. /// public sealed class ClosingLineValueCalculatorTests { private static readonly EventId EventId = new("11111111"); [Fact] public void Compute_Should_ReturnPositive_When_TakenRate_BeatsClose() { // Taken 2.20 (implied 0.4545); closed 2.00 (implied 0.5000) → CLV = +0.0455 var clv = ClosingLineValueCalculator.Compute(takenRate: 2.20m, closingRate: 2.00m); clv.Should().BeGreaterThan(0m); clv.Should().BeApproximately(0.04545m, 0.00001m); } [Fact] public void Compute_Should_ReturnNegative_When_TakenRate_WorseThanClose() { // Taken 1.80 (0.5556); closed 2.00 (0.5000) → CLV = -0.0556 var clv = ClosingLineValueCalculator.Compute(takenRate: 1.80m, closingRate: 2.00m); clv.Should().BeLessThan(0m); clv.Should().BeApproximately(-0.05556m, 0.00001m); } [Fact] public void Compute_Should_ReturnZero_When_RatesMatch() { ClosingLineValueCalculator.Compute(2.00m, 2.00m).Should().Be(0m); } [Theory] [InlineData(0)] [InlineData(-1)] public void Compute_Should_Throw_When_AnyRateIsZeroOrNegative(decimal rate) { ((Action)(() => ClosingLineValueCalculator.Compute(rate, 2m))) .Should().Throw(); ((Action)(() => ClosingLineValueCalculator.Compute(2m, rate))) .Should().Throw(); } [Fact] public void TryCompute_Should_ReturnNull_When_NoMatchingBetInSnapshot() { var taken = new Bet(MatchScope.Instance, BetType.Win, Side.Side1, value: null, new OddsRate(2.20m)); // Snapshot contains only Side2 — no match for Side1 Win. var snapshot = new OddsSnapshot(EventId, DateTimeOffset.UtcNow, OddsSource.PreMatch, new[] { new Bet(MatchScope.Instance, BetType.Win, Side.Side2, null, new OddsRate(1.70m)) }); var clv = ClosingLineValueCalculator.TryCompute(2.20m, taken, snapshot); clv.Should().BeNull(); } [Fact] public void TryCompute_Should_ReturnNull_When_SnapshotIsNull() { var taken = new Bet(MatchScope.Instance, BetType.Win, Side.Side1, null, new OddsRate(2m)); ClosingLineValueCalculator.TryCompute(2m, taken, closingSnapshot: null).Should().BeNull(); } [Fact] public void TryCompute_Should_MatchOnScopeTypeSideAndValue() { // Two handicap markets with different thresholds — pick the right one. var taken = new Bet(MatchScope.Instance, BetType.WinFora, Side.Side1, new OddsValue(-1.5m), new OddsRate(2.20m)); var snapshot = new OddsSnapshot(EventId, DateTimeOffset.UtcNow, OddsSource.PreMatch, new[] { new Bet(MatchScope.Instance, BetType.WinFora, Side.Side1, new OddsValue(-2.5m), new OddsRate(3.50m)), // wrong threshold new Bet(MatchScope.Instance, BetType.WinFora, Side.Side1, new OddsValue(-1.5m), new OddsRate(2.00m)), // match }); var clv = ClosingLineValueCalculator.TryCompute(2.20m, taken, snapshot); clv.Should().BeApproximately(0.04545m, 0.00001m); } }