using Microsoft.EntityFrameworkCore; using TeleWave.Application.Programming.Groups; using TeleWave.Application.Programming.Templates; using TeleWave.Application.Programming.Templates.CopyTemplate; using TeleWave.Application.Programming.Templates.CreateTemplate; using TeleWave.Application.Programming.Templates.Validate; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Programming; using TeleWave.Infrastructure.Persistence; using Xunit; namespace TeleWave.Integration.Tests; /// /// Операции над шаблоном против настоящей БД: копирование на другой канал и проверки по правилам. /// Обе штуки — сплошные запросы EF и перекладывание графов, в юнит-тестах их не поймать. /// [Collection("postgres")] public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture) { [SkippableFact] public async Task Copy_MovesLayersSlotsAndJunctions_AndReplacesTargetGrid() { Skip.IfNot(fixture.Available, "Docker недоступен"); await using var seedDb = fixture.CreateContext(); var (sourceId, targetId, groupId) = await SeedPairAsync(seedDb); await using var db = fixture.CreateContext(); var result = await new CopyTemplateCommandHandler(db).Handle( new CopyTemplateCommand(sourceId, targetId), default ); Assert.True(result.IsSuccess); await db.SaveChangesAsync(); Assert.Equal(1, result.Value.Layers); Assert.Equal(1, result.Value.Slots); Assert.Equal(1, result.Value.Junctions); await using var verify = fixture.CreateContext(); var target = verify.Channels.Single(c => c.Id == targetId); var copied = verify .ScheduleTemplates.Include(t => t.Layers) .ThenInclude(l => l.Slots) .Single(t => t.ChannelId == targetId); // У приёмника ровно один шаблон, и канал смотрит именно на него. Assert.Equal(copied.Id, target.TemplateId); Assert.Single(verify.ScheduleTemplates.Where(t => t.ChannelId == targetId)); var slot = copied.Layers.SelectMany(l => l.Slots).Single(); // Группы общие — ссылка переносится как есть, а не копией группы. Assert.Equal(groupId, slot.GroupId); Assert.Single(verify.Groups.Where(g => g.Id == groupId)); // Стык переехал своей копией, и слот ссылается на неё, а не на стык чужого канала. var junction = verify.JunctionTemplates.Single(j => j.ChannelId == targetId); Assert.Equal(junction.Id, slot.JunctionAfterId); Assert.NotEqual( verify.JunctionTemplates.Single(j => j.ChannelId == sourceId).Id, slot.JunctionAfterId ); } [Fact] public void Copy_ToItself_IsRejectedByValidator() { var validator = new CopyTemplateCommandValidator(); var id = Guid.NewGuid(); Assert.False(validator.Validate(new CopyTemplateCommand(id, id)).IsValid); Assert.True(validator.Validate(new CopyTemplateCommand(id, Guid.NewGuid())).IsValid); } [SkippableFact] public async Task Validate_ReportsEmptyGroupAndGridGap() { Skip.IfNot(fixture.Available, "Docker недоступен"); await using var seedDb = fixture.CreateContext(); var (sourceId, _, _) = await SeedPairAsync(seedDb); await using var db = fixture.CreateContext(); var result = await new ValidateTemplateQueryHandler( db, new DynamicGroupResolver(new GroupFilterMatcher(db), new GroupElementResolver(db)) ).Handle(new ValidateTemplateQuery(sourceId), default); Assert.True(result.IsSuccess); // Группа в сиде пустая, а слот занимает лишь два часа суток — обе проверки должны сработать. Assert.Contains(result.Value, i => i.Kind == TemplateIssueKind.GroupEmpty); Assert.Contains(result.Value, i => i.Kind == TemplateIssueKind.GridGap); } [SkippableFact] public async Task CreateTemplate_ForChannelWithoutGrid_LinksItAndIsIdempotent() { Skip.IfNot(fixture.Available, "Docker недоступен"); // Канал из старой ротации: сетки нет, ссылки на неё тоже. await using var seedDb = fixture.CreateContext(); var suffix = Guid.NewGuid().ToString("N")[..8]; var channel = Channel.Create( $"Без сетки {suffix}", $"nogrid-{suffix}", DateTimeOffset.UtcNow ); seedDb.Channels.Add(channel); await seedDb.SaveChangesAsync(); await using var db = fixture.CreateContext(); var created = await new CreateChannelTemplateCommandHandler(db).Handle( new CreateChannelTemplateCommand(channel.Id), default ); Assert.True(created.IsSuccess); await db.SaveChangesAsync(); await using var again = fixture.CreateContext(); var second = await new CreateChannelTemplateCommandHandler(again).Handle( new CreateChannelTemplateCommand(channel.Id), default ); await again.SaveChangesAsync(); // Повторный вызов возвращает ту же сетку: одна на канал, второй не появляется. Assert.True(second.IsSuccess); Assert.Equal(created.Value, second.Value); await using var verify = fixture.CreateContext(); var template = verify.ScheduleTemplates.Single(t => t.ChannelId == channel.Id); Assert.Equal(template.Id, verify.Channels.Single(c => c.Id == channel.Id).TemplateId); // Фоновый слой заводится сразу — без него первую же дыру в сетке нечем закрыть. Assert.Single(verify.GridLayers.Where(l => l.TemplateId == template.Id && l.IsBackground)); } /// Два канала: у источника слой со слотом, группой и стыком; у приёмника — пустая сетка. private static async Task<(Guid Source, Guid Target, Guid GroupId)> SeedPairAsync( AppDbContext db ) { var suffix = Guid.NewGuid().ToString("N")[..8]; var group = Group.Create($"Группа {suffix}"); db.Groups.Add(group); var source = Channel.Create($"Источник {suffix}", $"src-{suffix}", DateTimeOffset.UtcNow); var target = Channel.Create($"Приёмник {suffix}", $"dst-{suffix}", DateTimeOffset.UtcNow); var junction = JunctionTemplate.Create(source.Id, "Прайм"); var ad = junction.AddElement(JunctionElementKind.Ad); ad.Update(JunctionElementKind.Ad, group.Id, null, JunctionAmountMode.Count, 2, true, null); db.JunctionTemplates.Add(junction); var sourceTemplate = ScheduleTemplate.Create(source.Id, "Сетка источника"); var layer = sourceTemplate.AddLayer("Прайм", 10); var slot = layer.AddSlot("Вечернее кино", new TimeOnly(20, 0), 120); slot.UpdateContent( new SlotContent( slot.Title, SlotKind.Content, group.Id, new SlotStrategy(SlotStrategyType.Sequential).ToJson(), null, SlotBlockMode.FillSlot, 1, OverflowPolicy.ContinueNext, JunctionAfterId: junction.Id ) ); sourceTemplate.SetDefaultJunction(junction.Id); source.SetTemplate(sourceTemplate.Id); var targetTemplate = ScheduleTemplate.Create(target.Id, "Сетка приёмника"); target.SetTemplate(targetTemplate.Id); db.Channels.AddRange(source, target); db.ScheduleTemplates.AddRange(sourceTemplate, targetTemplate); await db.SaveChangesAsync(); return (source.Id, target.Id, group.Id); } }