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