using FluentAssertions; using Marathon.Application.Abstractions; using Marathon.Application.UseCases; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; namespace Marathon.Application.Tests.UseCases; public sealed class GetPendingAnomalyNotificationsUseCaseTests { private readonly IAnomalyRepository _anomalies = Substitute.For(); private readonly IEventRepository _events = Substitute.For(); private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); private static readonly DateTimeOffset BaseTime = new(2026, 5, 10, 18, 0, 0, MoscowOffset); public GetPendingAnomalyNotificationsUseCaseTests() { // Event titles are resolved via the batched GetManyAsync; route it through // the per-id GetAsync stubs each test configures. TestFixtures.BridgeGetMany(_events); } private GetPendingAnomalyNotificationsUseCase CreateSut() => new(_anomalies, _events, NullLogger.Instance); private static Anomaly MakeAnomaly(EventId id, decimal score, DateTimeOffset detectedAt) => new(Guid.NewGuid(), id, detectedAt, AnomalyKind.SuspensionFlip, score, "{}"); private static Event MakeEvent(EventId id) => new(id, new SportCode(11), "BY", "L1", "Cat", BaseTime, "Team A", "Team B"); private void StubAnomalies(params Anomaly[] anomalies) => _anomalies.ListByDateRangeAsync( Arg.Any(), Arg.Any(), Arg.Any()) .Returns(anomalies.ToList().AsReadOnly()); [Fact] public async Task Should_ExcludeAnomaliesBelowMinScore() { var id = new EventId("11111111"); StubAnomalies( MakeAnomaly(id, 0.30m, BaseTime), MakeAnomaly(id, 0.70m, BaseTime.AddMinutes(1))); _events.GetAsync(id, Arg.Any()).Returns(MakeEvent(id)); var result = await CreateSut().ExecuteAsync(BaseTime.AddHours(-1), minScore: 0.45m, CancellationToken.None); result.Should().ContainSingle(); result[0].Score.Should().Be(0.70m); result[0].EventTitle.Should().Be("Team A vs Team B"); } [Fact] public async Task Should_OrderOldestFirst() { var id1 = new EventId("11111111"); var id2 = new EventId("22222222"); StubAnomalies( MakeAnomaly(id2, 0.60m, BaseTime.AddMinutes(5)), MakeAnomaly(id1, 0.50m, BaseTime)); _events.GetAsync(id1, Arg.Any()).Returns(MakeEvent(id1)); _events.GetAsync(id2, Arg.Any()).Returns(MakeEvent(id2)); var result = await CreateSut().ExecuteAsync(BaseTime.AddHours(-1), 0.45m, CancellationToken.None); result.Select(n => n.DetectedAt).Should().BeInAscendingOrder(); } [Fact] public async Task Should_FallBackToEventId_When_EventMissing() { var id = new EventId("orphan00"); StubAnomalies(MakeAnomaly(id, 0.55m, BaseTime)); _events.GetAsync(id, Arg.Any()).Returns((Event?)null); var result = await CreateSut().ExecuteAsync(BaseTime.AddHours(-1), 0.45m, CancellationToken.None); result.Should().ContainSingle(); result[0].EventTitle.Should().Be("orphan00"); } [Fact] public async Task Should_ReturnEmpty_When_NothingQualifies() { StubAnomalies(Array.Empty()); var result = await CreateSut().ExecuteAsync(BaseTime, 0.45m, CancellationToken.None); result.Should().BeEmpty(); } }