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