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