using FluentAssertions; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; namespace Marathon.Domain.Tests.Entities; /// /// Invariants and derived-property tests for . /// public sealed class PlacedBetTests { private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); private static readonly DateTimeOffset MoscowMoment = new(2026, 5, 16, 12, 0, 0, MoscowOffset); private static PlacedBet Make(BetOutcome outcome, decimal rate = 2.10m, decimal stake = 100m) => new( Id: Guid.NewGuid(), EventId: new EventId("12345678"), Selection: new Bet(MatchScope.Instance, BetType.Win, Side.Side1, value: null, new OddsRate(rate)), Stake: stake, PlacedAt: MoscowMoment, Outcome: outcome, Notes: null); [Fact] public void Should_ComputeWonReturn_As_StakeTimesRate() { var bet = Make(BetOutcome.Won, rate: 2.10m, stake: 100m); bet.GrossReturn.Should().Be(210m); bet.NetProfit.Should().Be(110m); } [Fact] public void Should_ComputeLossReturn_As_Zero() { var bet = Make(BetOutcome.Lost, stake: 50m); bet.GrossReturn.Should().Be(0m); bet.NetProfit.Should().Be(-50m); } [Fact] public void Should_ReturnStake_When_Outcome_IsVoid() { var bet = Make(BetOutcome.Void, stake: 75m); bet.GrossReturn.Should().Be(75m); bet.NetProfit.Should().Be(0m); } [Fact] public void Should_ReturnNullProfit_When_OutcomeIsPending() { var bet = Make(BetOutcome.Pending); bet.GrossReturn.Should().BeNull(); bet.NetProfit.Should().BeNull(); } [Fact] public void WithOutcome_Should_ReturnNewInstance_With_GradedOutcome() { var pending = Make(BetOutcome.Pending); var graded = pending.WithOutcome(BetOutcome.Won); graded.Should().NotBeSameAs(pending); graded.Outcome.Should().Be(BetOutcome.Won); graded.Id.Should().Be(pending.Id); graded.Stake.Should().Be(pending.Stake); } [Fact] public void Should_Throw_When_StakeIsZeroOrNegative() { var act = () => new PlacedBet( Id: Guid.NewGuid(), EventId: new EventId("11111111"), Selection: new Bet(MatchScope.Instance, BetType.Win, Side.Side1, null, new OddsRate(2m)), Stake: 0m, PlacedAt: MoscowMoment, Outcome: BetOutcome.Pending, Notes: null); act.Should().Throw().WithMessage("*Stake*"); } [Fact] public void Should_Throw_When_PlacedAt_IsNotMoscowOffset() { var act = () => new PlacedBet( Id: Guid.NewGuid(), EventId: new EventId("11111111"), Selection: new Bet(MatchScope.Instance, BetType.Win, Side.Side1, null, new OddsRate(2m)), Stake: 100m, PlacedAt: DateTimeOffset.UtcNow, // UTC offset, not Moscow Outcome: BetOutcome.Pending, Notes: null); act.Should().Throw().WithMessage("*Moscow*"); } [Fact] public void Should_NormaliseWhitespace_Notes_To_Null() { var bet = new PlacedBet( Id: Guid.NewGuid(), EventId: new EventId("11111111"), Selection: new Bet(MatchScope.Instance, BetType.Win, Side.Side1, null, new OddsRate(2m)), Stake: 100m, PlacedAt: MoscowMoment, Outcome: BetOutcome.Pending, Notes: " "); bet.Notes.Should().BeNull(); } }