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, 3) }; // 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); // Окно целиком внутри записи — DISCONTINUITY-SEQUENCE равен её накопительному номеру. Assert.Equal(3, playlist.DiscontinuitySequence); } [Fact] public void EntryBoundary_InsertsDiscontinuity() { Guid a = Guid.NewGuid(), b = Guid.NewGuid(); var entries = new List { new(Epoch, Epoch.AddSeconds(16), a, 8, 5), // g0..7 new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12, 6), // 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) ); // Первый сегмент окна принадлежит A — база берётся из его номера. Assert.Equal(5, playlist.DiscontinuitySequence); } [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)); // Чисто-филлерное окно склеек не содержит — DISCONTINUITY-SEQUENCE равен нулю. Assert.Equal(0, playlist.DiscontinuitySequence); } [Fact] public void LeadingGap_WithoutFiller_TrimmedFromWindowStart() { var b = Guid.NewGuid(); var entries = new List { new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12, 4), }; // Окно [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); Assert.Equal(4, playlist.DiscontinuitySequence); } [Fact] public void FillerBetweenEntries_KeepsDiscontinuitySequenceConsistentAsWindowSlides() { // A g0..4, разрыв g5 (крутится филлер), B с g6. Номера записей отстоят на 2 (вход-в-филлер + // выход-из-него) — ровно столько склеек и вставит калькулятор между ними. Guid a = Guid.NewGuid(), b = Guid.NewGuid(); var entries = new List { new(Epoch, Epoch.AddSeconds(10), a, 5, 10), // g0..4 new(Epoch.AddSeconds(12), Epoch.AddSeconds(40), b, 14, 12), // g6.. }; var filler = new LiveFiller(Guid.NewGuid(), 4); // Окно [4..8]: хвост A, филлер, голова B — база отматывается к номеру A. var mixed = LiveWindowCalculator.Build(Input(16, entries, filler)); Assert.Equal(10, mixed.DiscontinuitySequence); Assert.Equal( [false, true, true, false, false], mixed.Segments.Select(s => s.Discontinuity) ); // Окно [6..10]: только B — обе склейки (A→филлер, филлер→B) ушли за край, +2 к номеру. var afterwards = LiveWindowCalculator.Build(Input(20, entries, filler)); Assert.Equal(12, afterwards.DiscontinuitySequence); Assert.All(afterwards.Segments, s => Assert.Equal(b, s.AssetId)); } [Fact] public void BeforeEpoch_ReturnsEmpty() { var playlist = LiveWindowCalculator.Build(Input(-10, [])); Assert.Empty(playlist.Segments); } }