using Microsoft.Extensions.Options; using NSubstitute; using TeleWave.Application.Broadcast; using TeleWave.Application.Broadcast.Bumpers; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Streaming; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Images; using TeleWave.Domain.Library; using TeleWave.Domain.Programming; using TeleWave.Infrastructure.Persistence; using Xunit; namespace TeleWave.Application.Tests.Broadcast; /// /// Превью ТВ-заставки: рендерится каждый подблок блока, длительность выравнивается по длине /// сегмента HLS, а названия «из/в» берутся из групп этого канала — иначе превью показывало бы /// случайные шоу из библиотеки. /// public class BumperPreviewTests { private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); private static RenderBumperPreviewCommandHandler Handler( AppDbContext db, IBumperRenderer? renderer = null, IBumperTemplateStorage? storage = null, IImageStore? imageStore = null ) => new( db, renderer ?? Substitute.For(), storage ?? Substitute.For(), imageStore ?? Substitute.For(), Options.Create(new BumperOptions()), Options.Create(new StreamingOptions { SegmentSeconds = 5 }) ); [Fact] public async Task Render_UnknownChannel_ReturnsNotFound() { var fixture = new TestDb(); await using var db = fixture.New(); var result = await Handler(db) .Handle( new RenderBumperPreviewCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None ); Assert.Equal(ChannelErrors.NotFound, result.Error); } [Fact] public async Task Render_UnknownTemplate_ReturnsBumperTemplateNotFound() { var fixture = new TestDb(); var channel = Channel.Create("Первый", "one", T0); await using (var seed = fixture.New()) { seed.Channels.Add(channel); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await Handler(db) .Handle( new RenderBumperPreviewCommand(channel.Id, Guid.NewGuid()), CancellationToken.None ); Assert.Equal(ChannelErrors.BumperTemplateNotFound, result.Error); } [Fact] public async Task Render_DrawsEveryVariant_WithChannelShowNamesAndAlignedDuration() { var fixture = new TestDb(); var channel = Channel.Create("Первый", "one", T0); var template = channel.BumperTemplates[0]; var second = template.AddVariant("Второй подблок"); // Шоу канала: слот сетки ссылается на группу, в группе — шоу. var show = Show.Create("Наше шоу", ShowKind.Series); var group = Group.Create("Кино"); group.AddElement(GroupElementKind.Show, show.Id); var grid = ScheduleTemplate.Create(channel.Id, "Сетка"); var slot = grid.Background!.AddSlot("Круглосуточно", new TimeOnly(6, 0), 24 * 60); slot.UpdateContent( new SlotContent( slot.Title, SlotKind.Content, group.Id, null, null, SlotBlockMode.FillSlot, 1, OverflowPolicy.ContinueNext ) ); channel.SetTemplate(grid.Id); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.Shows.Add(show); seed.Groups.Add(group); seed.ScheduleTemplates.Add(grid); await seed.SaveChangesAsync(CancellationToken.None); } var renderer = Substitute.For(); await using var db = fixture.New(); var result = await Handler(db, renderer) .Handle( new RenderBumperPreviewCommand(channel.Id, template.Id), CancellationToken.None ); Assert.True(result.IsSuccess); // Оба подблока отрисованы, каждый — в свой ассет-превью. await renderer .Received(1) .RenderAsync( BumperPreview.AssetId(template.Variants[0].Id), Arg.Any(), Arg.Any() ); await renderer .Received(1) .RenderAsync( BumperPreview.AssetId(second.Id), Arg.Any(), Arg.Any() ); var specs = renderer .ReceivedCalls() .Select(c => c.GetArguments()[1]) .OfType() .ToList(); // Своего звука у блока нет: 8 секунд по умолчанию, выровненные вверх до сегмента в 5 сек. Assert.All(specs, s => Assert.Equal(10, s.DurationSeconds)); Assert.All(specs, s => Assert.Equal("Наше шоу", s.NowTitle)); // Второго шоу у канала нет — подставляется заглушка. Assert.All(specs, s => Assert.Equal("Второе шоу", s.NextTitle)); Assert.All(specs, s => Assert.Null(s.BackgroundFile)); Assert.All(specs, s => Assert.Null(s.PosterFile)); } [Fact] public async Task Render_WithBackgroundImage_ResolvesPathFromRegistry() { var fixture = new TestDb(); var channel = Channel.Create("Первый", "one", T0); var template = channel.BumperTemplates[0]; var image = Image.Create(ImageCategory.BumperBackground, ".png", "bg.png"); template.SetBackgroundImage(image.Id); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.Images.Add(image); await seed.SaveChangesAsync(CancellationToken.None); } var renderer = Substitute.For(); var imageStore = Substitute.For(); imageStore.ResolvePath(image.Id, ".png").Returns("/data/images/bg.png"); await using var db = fixture.New(); var result = await Handler(db, renderer, imageStore: imageStore) .Handle( new RenderBumperPreviewCommand(channel.Id, template.Id), CancellationToken.None ); Assert.True(result.IsSuccess); var spec = renderer .ReceivedCalls() .Select(c => c.GetArguments()[1]) .OfType() .First(); Assert.Equal("/data/images/bg.png", spec.BackgroundFile); // Шоу у канала нет — обе подписи заглушечные. Assert.Equal("Первое шоу", spec.NowTitle); } }