using FluentAssertions; using Marathon.Domain.Entities; using Marathon.Domain.ValueObjects; namespace Marathon.Domain.Tests.Entities; public sealed class EventTests { private static readonly EventId SampleId = new("26456117"); private static readonly SportCode SampleSport = new(6); private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); private static readonly DateTimeOffset ValidScheduledAt = new(2026, 5, 10, 18, 0, 0, MoscowOffset); private static Event CreateValidEvent(DateTimeOffset? scheduledAt = null) => new( SampleId, SampleSport, "RU", "nba-league-1", "Play-Offs", scheduledAt ?? ValidScheduledAt, "Арсенал", "Атлетико"); [Fact] public void Constructor_CreatesEvent_WhenAllParametersAreValid() { var evt = CreateValidEvent(); evt.Id.Should().Be(SampleId); evt.Sport.Should().Be(SampleSport); evt.ScheduledAt.Offset.Should().Be(MoscowOffset); } [Fact] public void Constructor_ThrowsArgumentException_WhenScheduledAtIsNotMoscowTime() { // UTC+0 — wrong offset var utcTime = new DateTimeOffset(2026, 5, 10, 15, 0, 0, TimeSpan.Zero); var act = () => CreateValidEvent(utcTime); act.Should().Throw() .WithParameterName("ScheduledAt"); } [Fact] public void Constructor_ThrowsArgumentException_WhenScheduledAtIsOtherOffset() { // UTC+2 — wrong offset (CEST, for example) var cestTime = new DateTimeOffset(2026, 5, 10, 17, 0, 0, TimeSpan.FromHours(2)); var act = () => CreateValidEvent(cestTime); act.Should().Throw() .WithParameterName("ScheduledAt"); } [Fact] public void ScheduledAt_Offset_IsMoscowTime() { var evt = CreateValidEvent(); evt.ScheduledAt.Offset.Should().Be(TimeSpan.FromHours(3)); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenIdIsNull() { var act = () => new Event(null!, SampleSport, "RU", "l1", "cat", ValidScheduledAt, "A", "B"); act.Should().Throw().WithParameterName("Id"); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenSportIsNull() { var act = () => new Event(SampleId, null!, "RU", "l1", "cat", ValidScheduledAt, "A", "B"); act.Should().Throw().WithParameterName("Sport"); } [Theory] [InlineData("")] [InlineData(" ")] public void Constructor_ThrowsArgumentException_WhenCountryCodeIsEmptyOrWhitespace(string code) { var act = () => new Event(SampleId, SampleSport, code, "l1", "cat", ValidScheduledAt, "A", "B"); act.Should().Throw().WithParameterName("CountryCode"); } [Theory] [InlineData("")] [InlineData(" ")] public void Constructor_ThrowsArgumentException_WhenLeagueIdIsEmptyOrWhitespace(string leagueId) { var act = () => new Event(SampleId, SampleSport, "RU", leagueId, "cat", ValidScheduledAt, "A", "B"); act.Should().Throw().WithParameterName("LeagueId"); } [Theory] [InlineData("")] [InlineData(" ")] public void Constructor_ThrowsArgumentException_WhenSide1NameIsEmptyOrWhitespace(string name) { var act = () => new Event(SampleId, SampleSport, "RU", "l1", "cat", ValidScheduledAt, name, "B"); act.Should().Throw().WithParameterName("Side1Name"); } [Theory] [InlineData("")] [InlineData(" ")] public void Constructor_ThrowsArgumentException_WhenSide2NameIsEmptyOrWhitespace(string name) { var act = () => new Event(SampleId, SampleSport, "RU", "l1", "cat", ValidScheduledAt, "A", name); act.Should().Throw().WithParameterName("Side2Name"); } [Fact] public void Category_CanBeEmptyString() { // Category is optional (deep breadcrumbs may not exist) var evt = new Event(SampleId, SampleSport, "RU", "l1", string.Empty, ValidScheduledAt, "A", "B"); evt.Category.Should().Be(string.Empty); } [Fact] public void Event_IsImmutable_NoSettablePublicProperties() { var eventType = typeof(Event); var settableProperties = eventType.GetProperties() .Where(p => p.CanWrite && p.GetSetMethod(nonPublic: false) is not null) .ToList(); settableProperties.Should().BeEmpty("Event must be immutable."); } }