using System.Text.Json; using System.Text.Json.Serialization; using TeleWave.Application.Broadcast; using TeleWave.Application.Programming.Planning.Trace; using TeleWave.Application.Programming.Templates; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Library; using TeleWave.Domain.Media; using TeleWave.Domain.Programming; using TeleWave.Domain.Programming.Planning; using Xunit; namespace TeleWave.Application.Tests.Programming; /// /// «Почему это здесь»: трейс пишется в момент генерации и разбирается вместе с текущим состоянием /// сетки. Слот мог быть с тех пор изменён или удалён — подписи тогда пустеют, но ответ остаётся. /// public class EntryTraceTests { private static readonly DateTimeOffset T0 = new(2026, 1, 1, 18, 0, 0, TimeSpan.Zero); /// Те же настройки, что при записи трейса генератором. private static readonly JsonSerializerOptions TraceJson = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter() }, }; [Fact] public async Task Trace_UnknownEntry_ReturnsNotFound() { var fixture = new TestDb(); await using var db = fixture.New(); var result = await new GetEntryTraceQueryHandler(db).Handle( new GetEntryTraceQuery(Guid.NewGuid()), CancellationToken.None ); Assert.Equal(ChannelErrors.NotFound, result.Error); } [Fact] public async Task Trace_ResolvesSlotLayerGroupAndJunction() { var fixture = new TestDb(); var channel = Channel.Create("Первый", "one", T0); var group = Group.Create("Кино"); var collection = Collection.Create("Франшиза"); var show = Show.Create("Фильм", ShowKind.Single); var asset = MediaAsset.Register("film.mkv", ".mkv", MediaSource.Upload); var junction = JunctionTemplate.Create("Прайм"); var template = ScheduleTemplate.Create(channel.Id, "Сетка"); var layer = template.AddLayer("Прайм", 10); var slot = layer.AddSlot("Вечернее кино", new TimeOnly(20, 0), 120); slot.UpdateContent( new SlotContent( slot.Title, SlotKind.Content, group.Id, new SlotStrategy(SlotStrategyType.RandomWithCooldown, CooldownDays: 14).ToJson(), null, SlotBlockMode.FillSlot, 1, OverflowPolicy.ContinueNext, JunctionAfterId: junction.Id ) ); var trace = new PlanTrace( slot.Id, SlotKind.Content, GroupElementKind.Show, show.Id, SlotStrategyKind.RandomWithCooldown, CandidatesAfterCooldown: 4, DriftMinutes: 3, Snapped: true ); var entry = ScheduleEntry.FromSlot( channel.Id, asset.Id, ScheduleEntryKind.Program, T0, T0.AddMinutes(90), new ScheduleEntryOrigin( show.Id, 2, slot.Id, JsonSerializer.Serialize(trace, TraceJson), collection.Id ), 0 ); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.Groups.Add(group); seed.Collections.Add(collection); seed.Shows.Add(show); seed.MediaAssets.Add(asset); seed.JunctionTemplates.Add(junction); seed.ScheduleTemplates.Add(template); seed.ScheduleEntries.Add(entry); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new GetEntryTraceQueryHandler(db).Handle( new GetEntryTraceQuery(entry.Id), CancellationToken.None ); Assert.True(result.IsSuccess); var dto = result.Value; Assert.Equal("Фильм", dto.ShowName); Assert.Equal(2, dto.EpisodeIndex); Assert.Equal("Франшиза", dto.CollectionName); Assert.Equal("Прайм", dto.LayerName); Assert.Equal(10, dto.LayerPriority); Assert.Equal("Вечернее кино", dto.SlotTitle); Assert.Equal(new TimeOnly(20, 0), dto.SlotTargetStart); Assert.Equal(120, dto.SlotDurationMinutes); Assert.Equal("Кино", dto.GroupName); Assert.Equal(SlotStrategyKind.RandomWithCooldown, dto.Strategy); // Остывание берётся из текущей стратегии слота, а число кандидатов — из трейса. Assert.Equal(14, dto.CooldownDays); Assert.Equal(4, dto.CandidatesAfterCooldown); Assert.Equal(3, dto.DriftMinutes); Assert.True(dto.Snapped); Assert.Equal("Прайм", dto.JunctionName); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("{не json}")] public async Task Trace_WithoutUsableTrace_ReturnsBareEntry(string? traceJson) { var fixture = new TestDb(); var channel = Channel.Create("Первый", "one", T0); var show = Show.Create("Фильм", ShowKind.Single); var asset = MediaAsset.Register("film.mkv", ".mkv", MediaSource.Upload); var entry = ScheduleEntry.FromSlot( channel.Id, asset.Id, ScheduleEntryKind.Program, T0, T0.AddMinutes(90), new ScheduleEntryOrigin(show.Id, 1, null, traceJson), 0 ); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.Shows.Add(show); seed.MediaAssets.Add(asset); seed.ScheduleEntries.Add(entry); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new GetEntryTraceQueryHandler(db).Handle( new GetEntryTraceQuery(entry.Id), CancellationToken.None ); // Старая запись (или битый трейс) — ответ есть, подписи пустые. Assert.True(result.IsSuccess); Assert.Equal("Фильм", result.Value.ShowName); Assert.Null(result.Value.LayerName); Assert.Null(result.Value.SlotTitle); Assert.Null(result.Value.Strategy); Assert.Equal(0, result.Value.DriftMinutes); Assert.False(result.Value.Snapped); } [Fact] public async Task Trace_WithDeletedSlot_KeepsWhatTraceRemembers() { var fixture = new TestDb(); var channel = Channel.Create("Первый", "one", T0); var asset = MediaAsset.Register("film.mkv", ".mkv", MediaSource.Upload); var trace = new PlanTrace( Guid.NewGuid(), SlotKind.Repeat, null, null, SlotStrategyKind.Sequential, null, DriftMinutes: 0, Snapped: false ); var entry = ScheduleEntry.FromSlot( channel.Id, asset.Id, ScheduleEntryKind.Program, T0, T0.AddMinutes(30), new ScheduleEntryOrigin(null, null, null, JsonSerializer.Serialize(trace, TraceJson)), 0 ); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.MediaAssets.Add(asset); seed.ScheduleEntries.Add(entry); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new GetEntryTraceQueryHandler(db).Handle( new GetEntryTraceQuery(entry.Id), CancellationToken.None ); Assert.True(result.IsSuccess); // Слота уже нет — из трейса остаётся его тип и стратегия, остальное пустеет. Assert.Equal(SlotKind.Repeat, result.Value.SlotKind); Assert.Equal(SlotStrategyKind.Sequential, result.Value.Strategy); Assert.Null(result.Value.SlotTitle); Assert.Null(result.Value.GroupName); Assert.Null(result.Value.JunctionName); Assert.Null(result.Value.ShowName); } }