using TeleWave.Domain.Broadcast.Live; using Xunit; namespace TeleWave.Domain.Tests.Broadcast; public class LiveWindowCalculatorTests { private static readonly DateTimeOffset Epoch = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); private const int Seg = 2; private const int Window = 5; private static LiveInput Input( double nowOffsetSeconds, IReadOnlyList entries, LiveFiller? filler = null ) => new( Now: Epoch.AddSeconds(nowOffsetSeconds), Epoch: Epoch, SegmentSeconds: Seg, WindowSegments: Window, Entries: entries, Filler: filler ); [Fact] public void SingleEntry_ProducesWindowWithMonotonicMediaSequence() { var asset = Guid.NewGuid(); var entries = new List { new(Epoch, Epoch.AddSeconds(100), asset, 50) }; // now = epoch + 20s → currentIndex = 10, окно [6..10]. var playlist = LiveWindowCalculator.Build(Input(20, entries)); Assert.Equal(6, playlist.MediaSequence); Assert.Equal(Seg, playlist.TargetDuration); Assert.Equal(5, playlist.Segments.Count); Assert.Equal([6, 7, 8, 9, 10], playlist.Segments.Select(s => s.LocalIndex)); Assert.All(playlist.Segments, s => Assert.Equal(asset, s.AssetId)); Assert.DoesNotContain(playlist.Segments, s => s.Discontinuity); } [Fact] public void EntryBoundary_InsertsDiscontinuity() { Guid a = Guid.NewGuid(), b = Guid.NewGuid(); var entries = new List { new(Epoch, Epoch.AddSeconds(16), a, 8), // g0..7 new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12), // g8.. }; var playlist = LiveWindowCalculator.Build(Input(20, entries)); Assert.Equal(6, playlist.MediaSequence); Assert.Equal([a, a, b, b, b], playlist.Segments.Select(s => s.AssetId)); // DISCONTINUITY только перед первым сегментом B (индекс 2 в окне). Assert.Equal( [false, false, true, false, false], playlist.Segments.Select(s => s.Discontinuity) ); } [Fact] public void Gap_WithFiller_LoopsFiller() { var filler = new LiveFiller(Guid.NewGuid(), 4); var playlist = LiveWindowCalculator.Build(Input(20, [], filler)); Assert.Equal(6, playlist.MediaSequence); Assert.Equal(5, playlist.Segments.Count); Assert.Equal([2, 3, 0, 1, 2], playlist.Segments.Select(s => s.LocalIndex)); Assert.All(playlist.Segments, s => Assert.Equal(filler.AssetId, s.AssetId)); } [Fact] public void LeadingGap_WithoutFiller_TrimmedFromWindowStart() { var b = Guid.NewGuid(); var entries = new List { new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12), }; // Окно [6..10]: g6,g7 без записи и без филлера → отброшены, старт с g8. var playlist = LiveWindowCalculator.Build(Input(20, entries)); Assert.Equal(8, playlist.MediaSequence); Assert.Equal(3, playlist.Segments.Count); Assert.Equal([0, 1, 2], playlist.Segments.Select(s => s.LocalIndex)); Assert.DoesNotContain(playlist.Segments, s => s.Discontinuity); } [Fact] public void BeforeEpoch_ReturnsEmpty() { var playlist = LiveWindowCalculator.Build(Input(-10, [])); Assert.Empty(playlist.Segments); } }