using TeleWave.Domain.Broadcast; using TeleWave.Domain.Broadcast.Scheduling; using Xunit; namespace TeleWave.Domain.Tests.Broadcast; public class SchedulePlannerTests { private static readonly DateTimeOffset Start = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); /// Источник случайности с фиксированной (циклической) последовательностью значений. private sealed class FixedRandom(params int[] values) : IRandomSource { private int _i; public int Next(int maxExclusive) => values.Length == 0 ? 0 : values[_i++ % values.Length] % maxExclusive; } private static Dictionary Durations(params (Guid Id, int Minutes)[] items) => items.ToDictionary(x => x.Id, x => TimeSpan.FromMinutes(x.Minutes)); private static readonly Guid DefaultTemplate = Guid.NewGuid(); private static readonly Guid DefaultVariant = Guid.NewGuid(); /// Конфиг заставок с одним дефолтным подблоком (8с) заданного триггера, если варианты не заданы. private static PlannerBumperConfig Bumper( bool enabled, BumperTrigger trigger, TimeSpan minInterval, BumperSelection selection = BumperSelection.Rotation, params PlannerBumperVariant[] variants ) => new( enabled, minInterval, selection, variants.Length == 0 ? [ new PlannerBumperVariant( DefaultVariant, DefaultTemplate, TimeSpan.FromSeconds(8), trigger ), ] : variants ); [Fact] public void Count_Block_ProducesConsecutiveEpisodes_BackToBack() { var cs = Guid.NewGuid(); var show = Guid.NewGuid(); Guid[] eps = [Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()]; var input = new PlannerInput( ChannelId: Guid.NewGuid(), AdInsertion: AdInsertion.BetweenBlocks, AdsPerBreak: 1, NextAdIndex: 0, Shows: [new PlannerShow(cs, show, 1, BlockMode.Count, 3, eps, 0)], AdPool: [], Durations: Durations((eps[0], 2), (eps[1], 2), (eps[2], 2), (eps[3], 2)), Overrides: [], StartTime: Start, HorizonEnd: Start.AddSeconds(1) ); var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Equal(3, result.Entries.Count); Assert.All(result.Entries, e => Assert.Equal(ScheduleEntryKind.Program, e.Kind)); Assert.Equal([0, 1, 2], result.Entries.Select(e => e.EpisodeIndex)); Assert.Equal(Start, result.Entries[0].StartsAtUtc); Assert.Equal(result.Entries[0].EndsAtUtc, result.Entries[1].StartsAtUtc); Assert.Equal(result.Entries[1].EndsAtUtc, result.Entries[2].StartsAtUtc); Assert.Equal(3, result.NextEpisodeIndexByChannelShow[cs]); } [Fact] public void Count_Block_WrapsAtEndOfSeries() { var cs = Guid.NewGuid(); Guid[] eps = [Guid.NewGuid(), Guid.NewGuid()]; var input = BaseInput( [new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Count, 3, eps, 0)], Durations((eps[0], 2), (eps[1], 2)), horizonEnd: Start.AddSeconds(1) ); var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Equal([0, 1, 0], result.Entries.Select(e => e.EpisodeIndex)); Assert.Equal(1, result.NextEpisodeIndexByChannelShow[cs]); } [Fact] public void Duration_Block_TakesEpisodesUntilBudgetReached() { var cs = Guid.NewGuid(); Guid[] eps = [Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()]; var input = BaseInput( [new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Duration, 25, eps, 0)], Durations((eps[0], 10), (eps[1], 10), (eps[2], 10), (eps[3], 10)), horizonEnd: Start.AddMinutes(30) ); var result = SchedulePlanner.Plan(input, new FixedRandom(0)); // 10+10+10 = 30 >= 25 → три серии. Assert.Equal(3, result.Entries.Count); Assert.Equal([0, 1, 2], result.Entries.Select(e => e.EpisodeIndex)); Assert.Equal(3, result.NextEpisodeIndexByChannelShow[cs]); } [Fact] public void AdsBetweenBlocks_InsertsAdAfterBlock() { var cs = Guid.NewGuid(); Guid ep = Guid.NewGuid(), ad0 = Guid.NewGuid(), ad1 = Guid.NewGuid(); var input = new PlannerInput( Guid.NewGuid(), AdInsertion.BetweenBlocks, AdsPerBreak: 1, NextAdIndex: 0, Shows: [new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Count, 1, [ep], 0)], AdPool: [ad0, ad1], Durations: Durations((ep, 20), (ad0, 1), (ad1, 1)), Overrides: [], StartTime: Start, HorizonEnd: Start.AddSeconds(1) ); var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Equal(2, result.Entries.Count); Assert.Equal(ScheduleEntryKind.Program, result.Entries[0].Kind); Assert.Equal(ScheduleEntryKind.Ad, result.Entries[1].Kind); Assert.Equal(ad0, result.Entries[1].MediaAssetId); Assert.Equal(1, result.NextAdIndex); } [Fact] public void AdsBetweenEpisodes_InsertsAdAfterEachEpisode() { var cs = Guid.NewGuid(); Guid[] eps = [Guid.NewGuid(), Guid.NewGuid()]; Guid ad = Guid.NewGuid(); var input = new PlannerInput( Guid.NewGuid(), AdInsertion.BetweenEpisodes, AdsPerBreak: 1, NextAdIndex: 0, Shows: [new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Count, 2, eps, 0)], AdPool: [ad], Durations: Durations((eps[0], 20), (eps[1], 20), (ad, 1)), Overrides: [], StartTime: Start, HorizonEnd: Start.AddSeconds(1) ); var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Equal( [ ScheduleEntryKind.Program, ScheduleEntryKind.Ad, ScheduleEntryKind.Program, ScheduleEntryKind.Ad, ], result.Entries.Select(e => e.Kind) ); Assert.Equal(2, result.NextAdIndex); } [Fact] public void WeightedPick_RespectsRoll() { var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 3, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var pickA = SchedulePlanner.Plan( BaseInput([a, b], durations, Start.AddSeconds(1)), new FixedRandom(0) ); var pickB = SchedulePlanner.Plan( BaseInput([a, b], durations, Start.AddSeconds(1)), new FixedRandom(3) ); Assert.Equal(a.ShowId, pickA.Entries[0].ShowId); Assert.Equal(b.ShowId, pickB.Entries[0].ShowId); } [Fact] public void ExclusiveOverride_ForcesSingleShow() { var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 10, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var input = new PlannerInput( Guid.NewGuid(), AdInsertion.BetweenBlocks, 0, 0, [a, b], [], durations, Overrides: [ new PlannerOverride( OverrideMode.Exclusive, [new PlannerOverrideShow(b.ShowId, 1)], StartsAtUtc: Start, EndsAtUtc: Start.AddHours(1) ), ], StartTime: Start, HorizonEnd: Start.AddSeconds(1) ); var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Equal(b.ShowId, result.Entries[0].ShowId); } [Fact] public void Bumpers_InsertedOnTransitionBetweenDifferentShows_BackToBack() { var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with { Bumpers = Bumper(true, BumperTrigger.OnShowChange, TimeSpan.Zero), }; // Чередуем выбор: roll 0 → a, roll 1 → b (веса 1/1, total 2). var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1)); // Перед первым блоком заставки нет; далее по одной на каждый переход. Assert.Equal(ScheduleEntryKind.Program, result.Entries[0].Kind); var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList(); Assert.NotEmpty(bumpers); var first = result.Entries[1]; Assert.Equal(ScheduleEntryKind.Bumper, first.Kind); Assert.Equal(Guid.Empty, first.MediaAssetId); // ассет подставит оркестратор Assert.Equal(a.ShowId, first.FromShowId); Assert.Equal(b.ShowId, first.ToShowId); Assert.Equal(DefaultTemplate, first.BumperTemplateId); Assert.Equal(TimeSpan.FromSeconds(8), first.EndsAtUtc - first.StartsAtUtc); // Встык: программа → заставка → программа. Assert.Equal(result.Entries[0].EndsAtUtc, first.StartsAtUtc); Assert.Equal(first.EndsAtUtc, result.Entries[2].StartsAtUtc); } [Fact] public void Bumpers_SkippedForSameShow_WhenOnlyBetweenDifferentShows() { var cs = Guid.NewGuid(); Guid[] eps = [Guid.NewGuid(), Guid.NewGuid()]; var input = BaseInput( [new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Count, 1, eps, 0)], Durations((eps[0], 20), (eps[1], 20)), horizonEnd: Start.AddMinutes(50) ) with { Bumpers = Bumper(true, BumperTrigger.OnShowChange, TimeSpan.Zero), }; var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper); } [Fact] public void Bumpers_InsertedForSameShow_WhenNotRestrictedToDifferent() { var cs = Guid.NewGuid(); Guid[] eps = [Guid.NewGuid(), Guid.NewGuid()]; var input = BaseInput( [new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Count, 1, eps, 0)], Durations((eps[0], 20), (eps[1], 20)), horizonEnd: Start.AddMinutes(50) ) with { Bumpers = Bumper(true, BumperTrigger.Both, TimeSpan.Zero), }; var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Contains(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper); } [Fact] public void Bumpers_MinInterval_SuppressesTooFrequentBumpers() { var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); // Два перехода в горизонте (~на 20-й и ~40-й минуте), но интервал 30 мин пропускает второй. var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with { Bumpers = Bumper(true, BumperTrigger.OnShowChange, TimeSpan.FromMinutes(30)), }; var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1)); Assert.Equal(1, result.Entries.Count(e => e.Kind == ScheduleEntryKind.Bumper)); } [Fact] public void Bumpers_Rotation_CyclesTemplatesAndAdvancesCursor() { var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); Guid t0 = Guid.NewGuid(), t1 = Guid.NewGuid(); var input = BaseInput([a, b], durations, Start.AddMinutes(80)) with { Bumpers = Bumper( true, BumperTrigger.Both, TimeSpan.Zero, BumperSelection.Rotation, new PlannerBumperVariant( Guid.NewGuid(), t0, TimeSpan.FromSeconds(8), BumperTrigger.Both ), new PlannerBumperVariant( Guid.NewGuid(), t1, TimeSpan.FromSeconds(4), BumperTrigger.Both ) ), }; var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1)); var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList(); Assert.True(bumpers.Count >= 2); // Ротация: первый подблок → t0 (8с), второй → t1 (4с). Все — плейсхолдеры по паре шоу. Assert.Equal(t0, bumpers[0].BumperTemplateId); Assert.Equal(TimeSpan.FromSeconds(8), bumpers[0].EndsAtUtc - bumpers[0].StartsAtUtc); Assert.Equal(t1, bumpers[1].BumperTemplateId); Assert.Equal(TimeSpan.FromSeconds(4), bumpers[1].EndsAtUtc - bumpers[1].StartsAtUtc); Assert.All(bumpers, e => Assert.Equal(Guid.Empty, e.MediaAssetId)); Assert.Equal(bumpers.Count, result.NextBumperIndex); } [Fact] public void Bumpers_AlwaysFirst_AlwaysUsesFirstTemplate() { var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); Guid t0 = Guid.NewGuid(), t1 = Guid.NewGuid(); var input = BaseInput([a, b], durations, Start.AddMinutes(80)) with { Bumpers = Bumper( true, BumperTrigger.Both, TimeSpan.Zero, BumperSelection.AlwaysFirst, new PlannerBumperVariant( Guid.NewGuid(), t0, TimeSpan.FromSeconds(8), BumperTrigger.Both ), new PlannerBumperVariant( Guid.NewGuid(), t1, TimeSpan.FromSeconds(4), BumperTrigger.Both ) ), }; var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1)); var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList(); Assert.True(bumpers.Count >= 2); Assert.All(bumpers, e => Assert.Equal(t0, e.BumperTemplateId)); Assert.Equal(0, result.NextBumperIndex); // курсор ротации не двигается } [Fact] public void Bumpers_NoTemplates_NoBumpers() { var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with { Bumpers = new PlannerBumperConfig( true, MinInterval: TimeSpan.Zero, Selection: BumperSelection.Rotation, Variants: [] ), }; var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1)); Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper); } [Fact] public void Bumpers_Disabled_ProduceNoBumperEntries() { var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with { Bumpers = Bumper(false, BumperTrigger.OnShowChange, TimeSpan.Zero), }; var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1)); Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper); } [Fact] public void Bumpers_ShowChangeChanceZero_SuppressesBumpersOnShowChange() { // Два шоу, ротация выбора подблока (без random в пути заставки) → чередование гарантировано, // каждый переход — смена шоу. ShowChangeChance=0 глушит их, EpisodeChangeChance=1 не при чём. var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var input = BaseInput([a, b], durations, Start.AddMinutes(120)) with { Bumpers = Bumper(true, BumperTrigger.Both, TimeSpan.Zero) with { ShowChangeChance = 0.0, }, }; var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1)); Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper); } [Fact] public void Bumpers_EpisodeChangeChanceZero_SuppressesBumpersBetweenEpisodes() { // Одно шоу → каждый переход между блоками одного шоу. EpisodeChangeChance=0 глушит все заставки. var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20)); var input = BaseInput([a], durations, Start.AddMinutes(120)) with { Bumpers = Bumper(true, BumperTrigger.Both, TimeSpan.Zero) with { EpisodeChangeChance = 0.0, }, }; var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper); } [Fact] public void Bumpers_WeightedRandom_NeverPicksZeroWeightVariant() { // Взвешенный выбор: подблок с весом 0 не выбирается никогда, весь трафик уходит на t1 (вес 5). var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20)); Guid t0 = Guid.NewGuid(), t1 = Guid.NewGuid(); var input = BaseInput([a], durations, Start.AddMinutes(120)) with { Bumpers = Bumper( true, BumperTrigger.Both, TimeSpan.Zero, BumperSelection.WeightedRandom, new PlannerBumperVariant( Guid.NewGuid(), t0, TimeSpan.FromSeconds(8), BumperTrigger.Both, 0 ), new PlannerBumperVariant( Guid.NewGuid(), t1, TimeSpan.FromSeconds(8), BumperTrigger.Both, 5 ) ), }; var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1, 2, 3, 4)); var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList(); Assert.NotEmpty(bumpers); Assert.All(bumpers, e => Assert.Equal(t1, e.BumperTemplateId)); } [Fact] public void WeeklyOverride_AppliesOnMatchingDayAndTime() { // Start = 2026-01-01 (четверг, DayOfWeek=4), полночь. Еженедельный override на четверг 00:00–01:00. var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var input = BaseInput([a, b], durations, Start.AddSeconds(1)) with { Overrides = [ new PlannerOverride( OverrideMode.Exclusive, [new PlannerOverrideShow(b.ShowId, 1)], OverrideRecurrence.Weekly, DayOfWeek: (int)Start.UtcDateTime.DayOfWeek, StartMinute: 0, EndMinute: 60 ), ], }; var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Equal(b.ShowId, result.Entries[0].ShowId); } [Fact] public void WeeklyOverride_IgnoredOnOtherDay() { // Override на другой день недели → базовая ротация (FixedRandom(0) берёт первое шоу — a). var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var otherDay = ((int)Start.UtcDateTime.DayOfWeek + 1) % 7; var input = BaseInput([a, b], durations, Start.AddSeconds(1)) with { Overrides = [ new PlannerOverride( OverrideMode.Exclusive, [new PlannerOverrideShow(b.ShowId, 1)], OverrideRecurrence.Weekly, DayOfWeek: otherDay, StartMinute: 0, EndMinute: 1440 ), ], }; var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Equal(a.ShowId, result.Entries[0].ShowId); } [Fact] public void PreferredHours_BoostsWeight_InsideWindow() { // Час старта — 0 (UTC). Окно b покрывает его, множитель 100 → b доминирует, хотя базовые веса равны. var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0, PreferredHours: [new PlannerHourWindow(0, 24)], PreferredWeightMultiplier: 100 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var input = BaseInput([a, b], durations, Start.AddMinutes(60)); var result = SchedulePlanner.Plan(input, new FixedRandom(50)); var programs = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Program).ToList(); Assert.NotEmpty(programs); Assert.All(programs, e => Assert.Equal(b.ShowId, e.ShowId)); } [Fact] public void PreferredHours_OutsideWindow_KeepsBaseWeight() { // Час старта — 0, окно b — [10,12): не покрывает → буста нет, при FixedRandom(50) берётся первое шоу (a). var a = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0 ); var b = new PlannerShow( Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0, PreferredHours: [new PlannerHourWindow(10, 12)], PreferredWeightMultiplier: 100 ); var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20)); var input = BaseInput([a, b], durations, Start.AddMinutes(60)); var result = SchedulePlanner.Plan(input, new FixedRandom(50)); var programs = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Program).ToList(); Assert.NotEmpty(programs); Assert.All(programs, e => Assert.Equal(a.ShowId, e.ShowId)); } [Fact] public void NoPlayableShows_ReturnsEmpty() { var input = BaseInput([], new Dictionary(), Start.AddHours(1)); var result = SchedulePlanner.Plan(input, new FixedRandom(0)); Assert.Empty(result.Entries); } private static PlannerInput BaseInput( IReadOnlyList shows, IReadOnlyDictionary durations, DateTimeOffset horizonEnd ) => new( ChannelId: Guid.NewGuid(), AdInsertion: AdInsertion.BetweenBlocks, AdsPerBreak: 0, NextAdIndex: 0, Shows: shows, AdPool: [], Durations: durations, Overrides: [], StartTime: Start, HorizonEnd: horizonEnd ); }