using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.GetTemplate;
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 GetTemplateTests
{
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
[Fact]
public async Task ReturnsLayersByPriority_WithGroupNamesAndState()
{
var fixture = new TestDb();
var group = Group.Create("Кино");
var channel = Channel.Create("Первый", "first", T0);
channel.UpdateTimeSettings(1, 180, new TimeOnly(6, 0));
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
channel.SetTemplate(template.Id);
var top = template.AddLayer("Выходные", 20);
var slot = top.AddSlot("Кино в прайм", new TimeOnly(20, 0), 120, Daypart.Prime, weekday: 6);
slot.UpdateContent(
new SlotContent(
slot.Title,
SlotKind.Content,
group.Id,
new SlotStrategy(SlotStrategyType.RandomWithCooldown, CooldownDays: 14).ToJson(),
null,
SlotBlockMode.Count,
1,
OverflowPolicy.ContinueNext
)
);
template.MarkChanged();
await using (var seed = fixture.New())
{
seed.Groups.Add(group);
seed.Channels.Add(channel);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new GetChannelTemplateQueryHandler(db).Handle(
new GetChannelTemplateQuery(channel.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
var dto = result.Value;
// Слои идут сверху вниз: первым тот, что перекрывает остальные.
Assert.Equal(["Выходные", "Фон"], dto.Layers.Select(l => l.Name));
Assert.Equal(new TimeOnly(6, 0), dto.DayStartTime);
Assert.Equal(180, dto.UtcOffsetMinutes);
var stored = Assert.Single(dto.Layers[0].Slots);
// Имя группы приезжает вместе со слотом: инспектор показывает его без второго запроса.
Assert.Equal("Кино", stored.GroupName);
Assert.Equal(6, stored.Weekday);
Assert.Equal(SlotStrategyType.RandomWithCooldown, stored.Strategy!.Type);
Assert.Equal(14, stored.Strategy.CooldownDays);
// Правка эфир не двигает — она поднимает ревизию, и плашка об этом видна в шапке канала.
Assert.True(dto.HasPendingChanges);
Assert.Equal(1, dto.Revision);
Assert.Equal(0, dto.AppliedRevision);
// Сетку ни разу не применяли — откатывать не к чему, и кнопки быть не должно.
Assert.False(dto.CanRestore);
}
[Fact]
public async Task CanRestore_AfterFirstApply()
{
var fixture = new TestDb();
var channel = Channel.Create("Первый", "first", T0);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
channel.SetTemplate(template.Id);
template.MarkApplied(TemplateSnapshot.Capture(template).ToJson());
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new GetChannelTemplateQueryHandler(db).Handle(
new GetChannelTemplateQuery(channel.Id),
CancellationToken.None
);
Assert.True(result.Value.CanRestore);
Assert.False(result.Value.HasPendingChanges);
}
[Fact]
public async Task Fails_WhenChannelOrTemplateIsMissing()
{
var fixture = new TestDb();
var channel = Channel.Create("Без сетки", "no-grid", T0);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var handler = new GetChannelTemplateQueryHandler(db);
var noChannel = await handler.Handle(
new GetChannelTemplateQuery(Guid.NewGuid()),
CancellationToken.None
);
var noTemplate = await handler.Handle(
new GetChannelTemplateQuery(channel.Id),
CancellationToken.None
);
Assert.False(noChannel.IsSuccess);
Assert.False(noTemplate.IsSuccess);
}
[Fact]
public async Task ElementResolver_ExpandsCollectionIntoItsParts()
{
// Коллекция в группе — одна позиция, но эфира она даёт столько же, сколько все её части.
var fixture = new TestDb();
var film = Show.Create("Часть 1", ShowKind.Single);
film.SetAudience(ShowAudience.Pg13);
var sequel = Show.Create("Часть 2", ShowKind.Single);
sequel.SetAudience(ShowAudience.R);
var assets = new List();
foreach (var show in new[] { film, sequel })
{
var asset = MediaAsset.Register($"{show.Name}.mkv", ".mkv", MediaSource.Upload);
asset.MarkProcessing();
asset.MarkReady(
new MediaReadyInfo(
TimeSpan.FromMinutes(90),
6,
900,
1920,
1080,
"h264",
"aac",
$"assets/{show.Name}"
)
);
assets.Add(asset);
show.AddEpisode(asset.Id);
}
var collection = Collection.Create("Франшиза");
collection.AddShow(film.Id);
collection.AddShow(sequel.Id);
await using (var seed = fixture.New())
{
seed.MediaAssets.AddRange(assets);
seed.Shows.AddRange(film, sequel);
seed.Collections.Add(collection);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var info = await new GroupElementResolver(db).ResolveAsync(
[(GroupElementKind.Collection, collection.Id), (GroupElementKind.Show, film.Id)],
CancellationToken.None
);
var franchise = info[(GroupElementKind.Collection, collection.Id)];
Assert.Equal("Франшиза", franchise.Name);
Assert.Equal(2, franchise.UnitCount);
Assert.Equal(TimeSpan.FromMinutes(180), franchise.TotalDuration);
// Рейтинг франшизы — самый строгий среди частей: по нему её отсеивают в детское время.
Assert.Equal(ShowAudience.R, franchise.Audience);
var single = info[(GroupElementKind.Show, film.Id)];
Assert.Equal(1, single.UnitCount);
Assert.Equal(ShowKind.Single, single.ShowKind);
}
}