using FluentAssertions; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; namespace Marathon.Domain.Tests.Entities; public sealed class BetTests { private static readonly BetScope MatchScope = Marathon.Domain.ValueObjects.MatchScope.Instance; private static readonly OddsRate SampleRate = new(1.85m); private static readonly OddsValue SampleValue = new(3.5m); // ── Win bets ────────────────────────────────────────────────────────────── [Theory] [InlineData(Side.Side1)] [InlineData(Side.Side2)] public void Constructor_CreatesWinBet_WhenSideIs1Or2AndValueIsNull(Side side) { var bet = new Bet(MatchScope, BetType.Win, side, null, SampleRate); bet.Type.Should().Be(BetType.Win); bet.Side.Should().Be(side); bet.Value.Should().BeNull(); } [Theory] [InlineData(Side.Draw)] [InlineData(Side.Less)] [InlineData(Side.More)] public void Constructor_ThrowsArgumentException_WhenWinBetHasInvalidSide(Side side) { var act = () => new Bet(MatchScope, BetType.Win, side, null, SampleRate); act.Should().Throw() .WithParameterName("side"); } [Fact] public void Constructor_ThrowsArgumentException_WhenWinBetHasNonNullValue() { var act = () => new Bet(MatchScope, BetType.Win, Side.Side1, SampleValue, SampleRate); act.Should().Throw() .WithParameterName("value"); } // ── Draw bets ───────────────────────────────────────────────────────────── [Fact] public void Constructor_CreatesDrawBet_WhenSideIsDrawAndValueIsNull() { var bet = new Bet(MatchScope, BetType.Draw, Side.Draw, null, SampleRate); bet.Type.Should().Be(BetType.Draw); bet.Side.Should().Be(Side.Draw); bet.Value.Should().BeNull(); } [Theory] [InlineData(Side.Side1)] [InlineData(Side.Side2)] [InlineData(Side.Less)] [InlineData(Side.More)] public void Constructor_ThrowsArgumentException_WhenDrawBetHasInvalidSide(Side side) { var act = () => new Bet(MatchScope, BetType.Draw, side, null, SampleRate); act.Should().Throw() .WithParameterName("side"); } [Fact] public void Constructor_ThrowsArgumentException_WhenDrawBetHasNonNullValue() { var act = () => new Bet(MatchScope, BetType.Draw, Side.Draw, SampleValue, SampleRate); act.Should().Throw() .WithParameterName("value"); } // ── WinFora bets ────────────────────────────────────────────────────────── [Theory] [InlineData(Side.Side1)] [InlineData(Side.Side2)] public void Constructor_CreatesWinForaBet_WhenSideIs1Or2AndValueIsNonNull(Side side) { var bet = new Bet(MatchScope, BetType.WinFora, side, SampleValue, SampleRate); bet.Type.Should().Be(BetType.WinFora); bet.Value.Should().NotBeNull(); } [Fact] public void Constructor_ThrowsArgumentException_WhenWinForaBetHasNullValue() { var act = () => new Bet(MatchScope, BetType.WinFora, Side.Side1, null, SampleRate); act.Should().Throw() .WithParameterName("value"); } [Theory] [InlineData(Side.Draw)] [InlineData(Side.Less)] [InlineData(Side.More)] public void Constructor_ThrowsArgumentException_WhenWinForaBetHasInvalidSide(Side side) { var act = () => new Bet(MatchScope, BetType.WinFora, side, SampleValue, SampleRate); act.Should().Throw() .WithParameterName("side"); } // ── Total bets ──────────────────────────────────────────────────────────── [Theory] [InlineData(Side.Less)] [InlineData(Side.More)] public void Constructor_CreatesTotalBet_WhenSideIsLessOrMoreAndValueIsNonNull(Side side) { var bet = new Bet(MatchScope, BetType.Total, side, SampleValue, SampleRate); bet.Type.Should().Be(BetType.Total); bet.Value.Should().NotBeNull(); } [Fact] public void Constructor_ThrowsArgumentException_WhenTotalBetHasNullValue() { var act = () => new Bet(MatchScope, BetType.Total, Side.Less, null, SampleRate); act.Should().Throw() .WithParameterName("value"); } [Theory] [InlineData(Side.Side1)] [InlineData(Side.Side2)] [InlineData(Side.Draw)] public void Constructor_ThrowsArgumentException_WhenTotalBetHasInvalidSide(Side side) { var act = () => new Bet(MatchScope, BetType.Total, side, SampleValue, SampleRate); act.Should().Throw() .WithParameterName("side"); } // ── Null guards ─────────────────────────────────────────────────────────── [Fact] public void Constructor_ThrowsArgumentNullException_WhenScopeIsNull() { var act = () => new Bet(null!, BetType.Win, Side.Side1, null, SampleRate); act.Should().Throw() .WithParameterName("scope"); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenRateIsNull() { var act = () => new Bet(MatchScope, BetType.Win, Side.Side1, null, null!); act.Should().Throw() .WithParameterName("rate"); } // ── Immutability ────────────────────────────────────────────────────────── [Fact] public void Bet_IsImmutable_NoSettablePublicProperties() { var betType = typeof(Bet); var settableProperties = betType.GetProperties() .Where(p => p.CanWrite && p.GetSetMethod(nonPublic: false) is not null) .ToList(); settableProperties.Should().BeEmpty( "Bet is a sealed record with only get-only properties — immutability is required."); } }