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);
}
}