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);
}
}