using TeleWave.Application.Programming.Templates; using TeleWave.Application.Programming.Templates.CreateSlot; using TeleWave.Application.Programming.Templates.Layers; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Library; using TeleWave.Domain.Media; using TeleWave.Domain.Programming; using Xunit; namespace TeleWave.Application.Tests.Programming; /// /// Ролики в сетке. Технически это те же шоу, поэтому группа рекламы неотличима от группы сериалов /// по типам — и оба места, куда её можно поставить (слот и аварийная группа), обязаны сказать «нет»: /// в эфире это полоса рекламы, подписанная как программа. /// public class InterstitialGroupGuardTests { private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); private sealed record World(TestDb Db, Guid LayerId, Guid TemplateId, Guid Ads, Guid Content); private static Show Ready(string name, ShowKind kind, List assets, int seconds) { var show = Show.Create(name, kind); var asset = MediaAsset.Register($"{name}.mkv", ".mkv", MediaSource.Upload); asset.MarkProcessing(); asset.MarkReady( new MediaReadyInfo( TimeSpan.FromSeconds(seconds), 6, 1000, 1920, 1080, "h264", "aac", $"assets/{name}" ) ); assets.Add(asset); show.AddEpisode(asset.Id); return show; } private static async Task SeedAsync() { var fixture = new TestDb(); var assets = new List(); var shows = new List(); var ads = Group.Create("Реклама"); for (var i = 0; i < 3; i++) { var clip = Ready($"Ролик {i}", ShowKind.Interstitial, assets, 20); shows.Add(clip); ads.AddElement(GroupElementKind.Show, clip.Id); } var series = Ready("Сериал", ShowKind.Series, assets, 1320); shows.Add(series); var content = Group.Create("Сериалы"); content.AddElement(GroupElementKind.Show, series.Id); var channel = Channel.Create("Первый", "one", T0); var template = ScheduleTemplate.Create(channel.Id, "Сетка"); var layer = template.AddLayer("Основная", 10); channel.SetTemplate(template.Id); await using var seed = fixture.New(); seed.MediaAssets.AddRange(assets); seed.Shows.AddRange(shows); seed.Groups.AddRange([ads, content]); seed.Channels.Add(channel); seed.ScheduleTemplates.Add(template); await seed.SaveChangesAsync(CancellationToken.None); return new World(fixture, layer.Id, template.Id, ads.Id, content.Id); } private static SlotInput Input(Guid groupId) => new( "Блок", null, new TimeOnly(20, 0), 60, Daypart.Prime, SlotKind.Content, groupId, null, null, SlotBlockMode.FillSlot, 1, OverflowPolicy.ContinueNext, false, 30, null, null, null ); [Fact] public async Task Slot_WithInterstitialGroup_IsRejected() { var world = await SeedAsync(); await using var db = world.Db.New(); var result = await new CreateSlotCommandHandler(GroupServices.Slots(db)).Handle( new CreateSlotCommand(world.LayerId, Input(world.Ads)), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(TemplateErrors.InterstitialGroupInSlot, result.Error); } [Fact] public async Task Slot_WithContentGroup_IsAccepted() { // Обратная сторона запрета: обычная группа обязана проходить, иначе признак «ролики» // ловил бы всё подряд. var world = await SeedAsync(); await using var db = world.Db.New(); var result = await new CreateSlotCommandHandler(GroupServices.Slots(db)).Handle( new CreateSlotCommand(world.LayerId, Input(world.Content)), CancellationToken.None ); Assert.True(result.IsSuccess); } [Fact] public async Task FallbackGroup_OfInterstitials_IsRejected() { // Аварийная группа закрывает каждую паузу эфира — из роликов она превращает в рекламу // остаток каждого слота. var world = await SeedAsync(); await using var db = world.Db.New(); var result = await new UpdateTemplateCommandHandler( db, GroupServices.Interstitials(db) ).Handle( new UpdateTemplateCommand(world.TemplateId, "Сетка", world.Ads, null, null), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(TemplateErrors.InterstitialFallbackGroup, result.Error); } }