using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Groups.Suggest;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;
using Xunit;
namespace TeleWave.Application.Tests.Programming;
///
/// Предложения групп: разбор библиотеки должен быть объяснимым — что показали, то и создастся.
///
public class GroupSuggestionsTests
{
[Fact]
public async Task SuggestsGenre_WhenAtLeastTwoShowsShareIt()
{
var fixture = new TestDb();
var cartoons = Genre.Create("Мультфильм", "animation");
var drama = Genre.Create("Драма", "drama");
var first = WithGenre(Show.Create("A", ShowKind.Series), cartoons);
var second = WithGenre(Show.Create("B", ShowKind.Series), cartoons);
// Один драматический — на группу не тянет.
var lonely = WithGenre(Show.Create("C", ShowKind.Single), drama);
await using (var seed = fixture.New())
{
seed.Genres.AddRange(cartoons, drama);
seed.Shows.AddRange(first, second, lonely);
await seed.SaveChangesAsync(CancellationToken.None);
}
var suggestions = await BuildAsync(fixture);
var genre = Assert.Single(suggestions, s => s.Kind == GroupSuggestionKind.Genre);
Assert.Equal("Мультфильм", genre.Name);
Assert.Equal(2, genre.ShowIds.Count);
Assert.Equal([cartoons.Id], genre.Filter!.GenreIds);
}
[Fact]
public async Task SuggestsBigSeries_Separately()
{
var fixture = new TestDb();
var big = Show.Create("Гравити Фолз", ShowKind.Series);
for (var i = 0; i < 20; i++)
big.AddEpisode(Guid.NewGuid());
var small = Show.Create("Короткое", ShowKind.Series);
small.AddEpisode(Guid.NewGuid());
await using (var seed = fixture.New())
{
seed.Shows.AddRange(big, small);
await seed.SaveChangesAsync(CancellationToken.None);
}
var suggestions = await BuildAsync(fixture);
var series = Assert.Single(suggestions, s => s.Kind == GroupSuggestionKind.BigSeries);
Assert.Equal("Гравити Фолз", series.Name);
Assert.Equal([big.Id], series.ShowIds);
// Правила у одиночного шоу нет: «именно это шоу» фильтром не выражается.
Assert.Null(series.Filter);
}
[Fact]
public async Task SkipsAudienceTier_WhenItCoversEverything()
{
// Если под «не строже PG» попадает вся библиотека, группа повторяет её — предлагать нечего.
var fixture = new TestDb();
var first = Show.Create("A", ShowKind.Series);
first.SetAudience(ShowAudience.G);
var second = Show.Create("B", ShowKind.Series);
second.SetAudience(ShowAudience.Pg);
await using (var seed = fixture.New())
{
seed.Shows.AddRange(first, second);
await seed.SaveChangesAsync(CancellationToken.None);
}
var suggestions = await BuildAsync(fixture);
Assert.DoesNotContain(suggestions, s => s.Kind == GroupSuggestionKind.Audience);
}
[Fact]
public async Task SuggestsAudienceTier_WhenStricterContentExists()
{
var fixture = new TestDb();
var kids = Show.Create("A", ShowKind.Series);
kids.SetAudience(ShowAudience.G);
var adult = Show.Create("B", ShowKind.Series);
adult.SetAudience(ShowAudience.R);
var unrated = Show.Create("C", ShowKind.Series);
await using (var seed = fixture.New())
{
seed.Shows.AddRange(kids, adult, unrated);
await seed.SaveChangesAsync(CancellationToken.None);
}
var suggestions = await BuildAsync(fixture);
var tier = suggestions.First(s => s.Kind == GroupSuggestionKind.Audience);
// Непроставленный рейтинг остаётся в выборке — как в правиле набора и в планировщике.
Assert.Equal(2, tier.ShowIds.Count);
Assert.Contains(kids.Id, tier.ShowIds);
Assert.Contains(unrated.Id, tier.ShowIds);
}
[Fact]
public async Task CreateFromSuggestion_SavesFilterAndComposition()
{
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);
await using (var seed = fixture.New())
{
seed.Genres.Add(genre);
seed.Shows.AddRange(first, second);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var handler = new CreateGroupFromSuggestionCommandHandler(
db,
Builder(db),
GroupServices.Stats(db)
);
var created = await handler.Handle(
new CreateGroupFromSuggestionCommand($"genre:{genre.Id:N}"),
CancellationToken.None
);
Assert.True(created.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
await using var check = fixture.New();
var group = check.Groups.Include(g => g.Items).Single();
Assert.Equal("Мультфильм", group.Name);
Assert.Equal(2, group.Items.Count);
Assert.All(group.Items, item => Assert.Equal(GroupElementKind.Show, item.ElementKind));
Assert.Equal([genre.Id], GroupFilter.FromJson(group.FilterJson)!.GenreIds);
// Повторное создание того же — конфликт: две одноимённые группы в слоте неразличимы.
var again = await handler.Handle(
new CreateGroupFromSuggestionCommand($"genre:{genre.Id:N}"),
CancellationToken.None
);
Assert.False(again.IsSuccess);
}
[Fact]
public async Task CreateFromSuggestion_FailsForUnknownKey()
{
var fixture = new TestDb();
await using (var seed = fixture.New())
{
seed.Shows.Add(Show.Create("A", ShowKind.Series));
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new CreateGroupFromSuggestionCommandHandler(
db,
Builder(db),
GroupServices.Stats(db)
).Handle(new CreateGroupFromSuggestionCommand("genre:нет-такого"), CancellationToken.None);
Assert.False(result.IsSuccess);
}
private static Show WithGenre(Show show, Genre genre)
{
show.SetGenres([genre.Id]);
return show;
}
private static GroupSuggestionBuilder Builder(Infrastructure.Persistence.AppDbContext db) =>
new(db, new GroupElementResolver(db));
private static async Task> BuildAsync(TestDb fixture)
{
await using var db = fixture.New();
return await Builder(db).BuildAsync(CancellationToken.None);
}
}