using Microsoft.EntityFrameworkCore; using TeleWave.Application.Programming.Groups; using TeleWave.Application.Programming.Groups.ExcludeGroupElement; using TeleWave.Application.Programming.Groups.UpdateGroup; using TeleWave.Application.Programming.Planning; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Library; using TeleWave.Domain.Media; using TeleWave.Domain.Programming; using Xunit; namespace TeleWave.Application.Tests.Programming; /// /// Динамическая группа: состав считает правило, а список позиций хранит только ручные поправки. /// Проверяем именно то, что уходит в эфир, — иначе расхождение между показанным и сыгранным. /// public class DynamicGroupsTests { [Fact] public async Task Composition_IsRuleResult_PinnedFirstThenAlphabetical() { var fixture = new TestDb(); var genre = Genre.Create("Мультфильм", "animation"); var beta = WithGenre(Show.Create("Бета", ShowKind.Series), genre); var alpha = WithGenre(Show.Create("Альфа", ShowKind.Series), genre); // Закреплённое правилу не подходит, но остаётся в составе — и идёт первым. var pinned = Show.Create("Закреплённое", ShowKind.Single); var group = Group.Create("Мультфильмы"); group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson()); group.SetMode(GroupMode.Dynamic); // Закрепляют уже в динамическом режиме: там список — это поправки к правилу. group.AddElement(GroupElementKind.Show, pinned.Id); await using (var seed = fixture.New()) { seed.Genres.Add(genre); seed.Shows.AddRange(alpha, beta, pinned); seed.Groups.Add(group); await seed.SaveChangesAsync(CancellationToken.None); } var composition = await ResolveAsync(fixture, group.Id); Assert.Equal([pinned.Id, alpha.Id, beta.Id], composition.Select(e => e.Id)); Assert.True(composition[0].Pinned); Assert.All(composition.Skip(1), e => Assert.False(e.Pinned)); } [Fact] public async Task Excluded_DropsOutOfComposition() { var fixture = new TestDb(); var genre = Genre.Create("Мультфильм", "animation"); var keep = WithGenre(Show.Create("Остаётся", ShowKind.Series), genre); var drop = WithGenre(Show.Create("Выкинуть", ShowKind.Series), genre); var group = Group.Create("Мультфильмы"); group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson()); group.SetMode(GroupMode.Dynamic); await using (var seed = fixture.New()) { seed.Genres.Add(genre); seed.Shows.AddRange(keep, drop); seed.Groups.Add(group); await seed.SaveChangesAsync(CancellationToken.None); } await using (var db = fixture.New()) { var result = await new ExcludeGroupElementCommandHandler( db, GroupServices.Stats(db) ).Handle( new ExcludeGroupElementCommand(group.Id, GroupElementKind.Show, drop.Id, true), CancellationToken.None ); Assert.True(result.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); } var composition = await ResolveAsync(fixture, group.Id); Assert.Equal([keep.Id], composition.Select(e => e.Id)); // Снятие исключения возвращает элемент правилу. await using (var db = fixture.New()) { await new ExcludeGroupElementCommandHandler(db, GroupServices.Stats(db)).Handle( new ExcludeGroupElementCommand(group.Id, GroupElementKind.Show, drop.Id, false), CancellationToken.None ); await db.SaveChangesAsync(CancellationToken.None); } Assert.Equal(2, (await ResolveAsync(fixture, group.Id)).Count); } [Fact] public async Task SwitchToDynamic_DropsTheOldListAndLetsTheRuleDecide() { // Переключили «по правилу» — значит состав считает правило. Прежний список, перенесённый // закреплённым, оставался бы в группе навсегда: сузить набор правилом было бы нечем, // и «динамическая» отличалась бы от статической только названием. var fixture = new TestDb(); var genre = Genre.Create("Мультфильм", "animation"); var byRule = WithGenre(Show.Create("По правилу", ShowKind.Series), genre); var old = Show.Create("Из старого списка", ShowKind.Series); var group = Group.Create("Группа"); group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson()); group.AddElement(GroupElementKind.Show, old.Id); await using (var seed = fixture.New()) { seed.Genres.Add(genre); seed.Shows.AddRange(byRule, old); seed.Groups.Add(group); await seed.SaveChangesAsync(CancellationToken.None); } await UpdateModeAsync(fixture, group.Id, GroupMode.Dynamic); await using var check = fixture.New(); var stored = check.Groups.Include(g => g.Items).Single(); Assert.Equal(GroupMode.Dynamic, stored.Mode); Assert.Empty(stored.Items); // В составе — только то, что нашло правило: шоу из прежнего списка под него не подходит. var composition = await ResolveAsync(fixture, group.Id); Assert.Equal([byRule.Id], composition.Select(e => e.Id)); } [Fact] public async Task SwitchToStatic_MaterializesComputedComposition() { var fixture = new TestDb(); var genre = Genre.Create("Мультфильм", "animation"); var first = WithGenre(Show.Create("A", ShowKind.Series), genre); var second = WithGenre(Show.Create("B", ShowKind.Series), genre); var group = Group.Create("Мультфильмы"); group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson()); group.SetMode(GroupMode.Dynamic); await using (var seed = fixture.New()) { seed.Genres.Add(genre); seed.Shows.AddRange(first, second); seed.Groups.Add(group); await seed.SaveChangesAsync(CancellationToken.None); } await UpdateModeAsync(fixture, group.Id, GroupMode.Static); await using var check = fixture.New(); var stored = check.Groups.Include(g => g.Items).Single(); Assert.Equal(GroupMode.Static, stored.Mode); Assert.Equal(2, stored.Items.Count); Assert.All(stored.Items, item => Assert.Equal(GroupItemRole.Member, item.Role)); } [Fact] public async Task Planner_ExpandsDynamicGroupByRule() { // Главное свойство: правило применяется на самой генерации, а не когда-то до неё. var fixture = new TestDb(); var genre = Genre.Create("Мультфильм", "animation"); var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload); asset.MarkProcessing(); asset.MarkReady( new MediaReadyInfo( TimeSpan.FromMinutes(22), 2, 660, 1920, 1080, "h264", "aac", "assets/a" ) ); var show = WithGenre(Show.Create("A", ShowKind.Series), genre); show.AddEpisode(asset.Id); var group = Group.Create("Мультфильмы"); group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson()); group.SetMode(GroupMode.Dynamic); await using (var seed = fixture.New()) { seed.Genres.Add(genre); seed.MediaAssets.Add(asset); seed.Shows.Add(show); seed.Groups.Add(group); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var expanded = await new GroupExpander(db, GroupServices.Dynamic(db)).ExpandAsync( [group.Id], Guid.NewGuid(), CancellationToken.None ); var element = Assert.Single(expanded[group.Id]); Assert.Equal(show.Id, element.ElementId); Assert.Single(element.Units); } private static Show WithGenre(Show show, Genre genre) { show.SetGenres([genre.Id]); return show; } private static async Task> ResolveAsync( TestDb fixture, Guid groupId ) { await using var db = fixture.New(); var group = await db.Groups.Include(g => g.Items).FirstAsync(g => g.Id == groupId); return await GroupServices.Dynamic(db).ResolveAsync(group, CancellationToken.None); } private static async Task UpdateModeAsync(TestDb fixture, Guid groupId, GroupMode mode) { await using var db = fixture.New(); var group = await db.Groups.FirstAsync(g => g.Id == groupId); var result = await new UpdateGroupCommandHandler( db, GroupServices.Dynamic(db), GroupServices.Stats(db) ).Handle( new UpdateGroupCommand( groupId, group.Name, group.Description, GroupFilter.FromJson(group.FilterJson), mode ), CancellationToken.None ); Assert.True(result.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); } }