using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Tests.Fakes; using NSubstitute; using NUnit.Framework; using Shouldly; namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling; [TestFixture] public class ContinuePlayoutTests : PlayoutBuilderTestBase { [Test] public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) }; (PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(1); result.AddedItems.Head().MediaItemId.ShouldBe(1); playout.Anchor.NextStartOffset.ShouldBe(finish); playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); PlayoutBuildResult result2 = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); result2.AddedItems.Count.ShouldBe(1); result2.AddedItems[0].StartOffset.ShouldBe(finish); result2.AddedItems[0].MediaItemId.ShouldBe(2); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(12)); playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(0); } [Test] public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) }; (PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(1); result.AddedItems.Head().MediaItemId.ShouldBe(1); playout.Anchor.NextStartOffset.ShouldBe(finish); playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); PlayoutBuildResult result2 = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); result2.AddedItems.Count.ShouldBe(2); result2.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(6)); result2.AddedItems[0].MediaItemId.ShouldBe(2); result2.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(12)); result2.AddedItems[1].MediaItemId.ShouldBe(1); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(18)); playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); } [Test] public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems_MultiDay() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) }; (PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromDays(1); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(4); result.AddedItems.Map(i => i.MediaItemId).ToList().ShouldBe([1, 2, 1, 2]); playout.Anchor.NextStartOffset.ShouldBe(finish); playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(0); PlayoutProgramScheduleAnchor headAnchor = playout.ProgramScheduleAnchors.Head(); // throw in a detractor anchor - playout builder should prioritize the "continue" anchor playout.ProgramScheduleAnchors.Insert( 0, new PlayoutProgramScheduleAnchor { Id = headAnchor.Id + 1, Collection = headAnchor.Collection, CollectionId = headAnchor.CollectionId, Playout = playout, PlayoutId = playout.Id, AnchorDate = DateTime.Today.ToUniversalTime(), CollectionType = headAnchor.CollectionType, EnumeratorState = new CollectionEnumeratorState { Index = headAnchor.EnumeratorState.Index + 1, Seed = headAnchor.EnumeratorState.Seed }, MediaItem = headAnchor.MediaItem, MediaItemId = headAnchor.MediaItemId, MultiCollection = headAnchor.MultiCollection, MultiCollectionId = headAnchor.MultiCollectionId, SmartCollection = headAnchor.SmartCollection, SmartCollectionId = headAnchor.SmartCollectionId }); // continue 1h later DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(1); result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); result.AddedItems.Count.ShouldBe(1); result.AddedItems[0].StartOffset.ShouldBe(finish); result.AddedItems[0].MediaItemId.ShouldBe(1); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(30)); playout.ProgramScheduleAnchors.Count.ShouldBe(2); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); // continue 1h later DateTimeOffset start3 = HoursAfterMidnight(2); DateTimeOffset finish3 = start3 + TimeSpan.FromDays(1); result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start3, finish3, CancellationToken); result.AddedItems.Count.ShouldBe(0); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(30)); playout.ProgramScheduleAnchors.Count.ShouldBe(2); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); } [Test] public async Task ShuffleFlood_Should_MaintainRandomSeed() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) }; (PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(6); playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed.ShouldBeGreaterThan(0); int firstSeedValue = playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed; playout.Anchor.NextStartOffset.ShouldBe(finish); DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); PlayoutBuildResult result2 = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); int secondSeedValue = playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed; firstSeedValue.ShouldBe(secondSeedValue); playout.Anchor.NextStartOffset.ShouldBe(finish); } [Test] public async Task ShuffleFlood_Should_MaintainRandomSeed_MultipleDays() { var mediaItems = new List(); for (var i = 1; i <= 25; i++) { mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); } (PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); DateTimeOffset finish = start + TimeSpan.FromDays(2); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(53); playout.ProgramScheduleAnchors.Count.ShouldBe(2); playout.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).ShouldBeTrue(); PlayoutProgramScheduleAnchor lastCheckpoint = playout.ProgramScheduleAnchors .OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue) .First(); lastCheckpoint.EnumeratorState.Seed.ShouldBeGreaterThan(0); lastCheckpoint.EnumeratorState.Index.ShouldBe(3); // we need to mess up the ordering to trigger the problematic behavior // this simulates the way the rows are loaded with EF PlayoutProgramScheduleAnchor oldest = playout.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate) .Last(); PlayoutProgramScheduleAnchor newest = playout.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate) .First(); playout.ProgramScheduleAnchors = [ oldest, newest ]; int firstSeedValue = lastCheckpoint.EnumeratorState.Seed; DateTimeOffset start2 = start.AddHours(1); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2); PlayoutBuildResult result2 = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); PlayoutProgramScheduleAnchor continueAnchor = playout.ProgramScheduleAnchors.First(x => x.AnchorDate is null); int secondSeedValue = continueAnchor.EnumeratorState.Seed; // the continue anchor should have the same seed as the most recent (last) checkpoint from the first run firstSeedValue.ShouldBe(secondSeedValue); } [Test] public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) }; (PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(6); playout.ProgramScheduleAnchors.Count.ShouldBe(2); PlayoutProgramScheduleAnchor primaryAnchor = playout.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1); primaryAnchor.EnumeratorState.Seed.ShouldBeGreaterThan(0); primaryAnchor.EnumeratorState.Index.ShouldBe(0); int firstSeedValue = primaryAnchor.EnumeratorState.Seed; DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); PlayoutBuildResult result2 = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); primaryAnchor = playout.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1); int secondSeedValue = primaryAnchor.EnumeratorState.Seed; firstSeedValue.ShouldBe(secondSeedValue); primaryAnchor.EnumeratorState.Index.ShouldBe(0); } [Test] public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed_MultipleDays() { var mediaItems = new List(); for (var i = 1; i <= 100; i++) { mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); } (PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); DateTimeOffset finish = start + TimeSpan.FromDays(2); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(53); playout.ProgramScheduleAnchors.Count.ShouldBe(4); playout.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).ShouldBeTrue(); PlayoutProgramScheduleAnchor lastCheckpoint = playout.ProgramScheduleAnchors .Filter(psa => psa.SmartCollectionId == 1) .OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue) .First(); lastCheckpoint.EnumeratorState.Seed.ShouldBeGreaterThan(0); lastCheckpoint.EnumeratorState.Index.ShouldBe(53); int firstSeedValue = lastCheckpoint.EnumeratorState.Seed; for (var i = 1; i < 20; i++) { DateTimeOffset start2 = start.AddHours(i); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2); PlayoutBuildResult result2 = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); PlayoutProgramScheduleAnchor continueAnchor = playout.ProgramScheduleAnchors .Filter(psa => psa.SmartCollectionId == 1) .First(x => x.AnchorDate is null); int secondSeedValue = continueAnchor.EnumeratorState.Seed; // the continue anchor should have the same seed as the most recent (last) checkpoint from the first run firstSeedValue.ShouldBe(secondSeedValue); } } [Test] public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor() { var floodCollection = new Collection { Id = 1, Name = "Flood Items", MediaItems = [ TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) ] }; var fixedCollection = new Collection { Id = 2, Name = "Fixed Items", MediaItems = [ TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) ] }; var fakeRepository = new FakeMediaCollectionRepository( Map( (floodCollection.Id, floodCollection.MediaItems.ToList()), (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = floodCollection, CollectionId = floodCollection.Id, StartTime = TimeSpan.FromHours(7), PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemOne { Id = 2, Index = 2, Collection = fixedCollection, CollectionId = fixedCollection.Id, StartTime = TimeSpan.FromHours(12), PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(9).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, InFlood = true }, ProgramScheduleAnchors = [], Items = [], ProgramScheduleAlternates = [], FillGroupIndices = [] }; var referenceData = new PlayoutReferenceData(playout.Channel, Option.None, [], [], playout.ProgramSchedule, [], []); IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, Logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(32); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(5); result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(9)); result.AddedItems[0].MediaItemId.ShouldBe(1); result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(10)); result.AddedItems[1].MediaItemId.ShouldBe(2); result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(11)); result.AddedItems[2].MediaItemId.ShouldBe(1); result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(12)); result.AddedItems[3].MediaItemId.ShouldBe(3); result.AddedItems[4].StartOffset.ShouldBe(start + TimeSpan.FromHours(31)); result.AddedItems[4].MediaItemId.ShouldBe(2); playout.Anchor.InFlood.ShouldBeTrue(); playout.Anchor.NextStartOffset.ShouldBe(finish); } [Test] public async Task Alternating_MultipleContent_Should_Maintain_Counts() { var collectionOne = new Collection { Id = 1, Name = "Multiple Items 1", MediaItems = [TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))] }; var collectionTwo = new Collection { Id = 2, Name = "Multiple Items 2", MediaItems = [TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))] }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemMultiple { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, Count = 3, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemMultiple { Id = 2, Index = 2, Collection = collectionTwo, CollectionId = collectionTwo.Id, StartTime = null, Count = 3, PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(1).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, MultipleRemaining = 2 }, ProgramScheduleAnchors = [], Items = [], ProgramScheduleAlternates = [], FillGroupIndices = [] }; var referenceData = new PlayoutReferenceData(playout.Channel, Option.None, [], [], playout.ProgramSchedule, [], []); IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, Logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(4); result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(1)); result.AddedItems[0].MediaItemId.ShouldBe(1); result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(2)); result.AddedItems[1].MediaItemId.ShouldBe(1); result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(3)); result.AddedItems[2].MediaItemId.ShouldBe(2); result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(4)); result.AddedItems[3].MediaItemId.ShouldBe(2); playout.Anchor.ScheduleItemsEnumeratorState.Index.ShouldBe(1); playout.Anchor.MultipleRemaining.ShouldBe(1); playout.Anchor.NextStartOffset.ShouldBe(finish); } [Test] public async Task Alternating_Duration_Should_Complete_Duration() { var collectionOne = new Collection { Id = 1, Name = "Duration Items 1", MediaItems = [TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))] }; var collectionTwo = new Collection { Id = 2, Name = "Duration Items 2", MediaItems = [TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))] }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemDuration { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(3), TailMode = TailMode.None, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemDuration { Id = 2, Index = 2, Collection = collectionTwo, CollectionId = collectionTwo.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(3), TailMode = TailMode.None, PlaybackOrder = PlaybackOrder.Chronological } }; DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = (start + TimeSpan.FromHours(1)).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, DurationFinish = (start + TimeSpan.FromHours(3)).UtcDateTime }, ProgramScheduleAnchors = [], Items = [], ProgramScheduleAlternates = [], FillGroupIndices = [] }; var referenceData = new PlayoutReferenceData(playout.Channel, Option.None, [], [], playout.ProgramSchedule, [], []); IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, Logger); PlayoutBuildResult result = await builder.Build( playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start, finish, CancellationToken); result.AddedItems.Count.ShouldBe(5); result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(1)); result.AddedItems[0].MediaItemId.ShouldBe(1); result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(2)); result.AddedItems[1].MediaItemId.ShouldBe(1); result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(3)); result.AddedItems[2].MediaItemId.ShouldBe(2); result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(4)); result.AddedItems[3].MediaItemId.ShouldBe(2); result.AddedItems[4].StartOffset.ShouldBe(start + TimeSpan.FromHours(5)); result.AddedItems[4].MediaItemId.ShouldBe(2); playout.Anchor.ScheduleItemsEnumeratorState.Index.ShouldBe(0); playout.Anchor.DurationFinish.ShouldBeNull(); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(6)); } }