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