using Microsoft.EntityFrameworkCore; using TeleWave.Application.Library.Collections; using TeleWave.Application.Library.Collections.AddShowsToCollection; using TeleWave.Application.Library.Collections.Suggest; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Library; using Xunit; namespace TeleWave.Application.Tests.Library; /// /// Сборка франшиз: предложения по метаданным и по названиям плюс массовое добавление в коллекцию. /// Ошибка здесь означает чужой фильм в середине франшизы, поэтому осторожность важнее полноты. /// public class CollectionSuggestionsTests { [Fact] public async Task SuggestsFranchise_FromMetadata() { var fixture = new TestDb(); var first = Movie("Гарри Поттер и философский камень", 2001, franchise: "1241"); var second = Movie("Гарри Поттер и тайная комната", 2002, franchise: "1241"); var other = Movie("Совсем другое", 1999, franchise: "777"); await SeedAsync(fixture, first, second, other); var suggestions = await BuildAsync(fixture); var franchise = Assert.Single( suggestions, s => s.Source == CollectionSuggestionSource.Metadata ); Assert.Equal("Франшиза 1241", franchise.Name); // Порядок — по году: это и есть порядок просмотра. Assert.Equal([first.Id, second.Id], franchise.Parts.Select(p => p.ShowId)); } [Fact] public async Task SuggestsByName_WhenMetadataMissing() { var fixture = new TestDb(); var first = Movie("Рокки", 1976); var second = Movie("Рокки 2", 1979); await SeedAsync(fixture, first, second); var suggestions = await BuildAsync(fixture); var guess = Assert.Single( suggestions, s => s.Source == CollectionSuggestionSource.NameGuess ); Assert.Equal(2, guess.Parts.Count); } [Fact] public async Task DoesNotGuess_OnShortCommonStart() { // «Человек-паук» и «Человек дождя» — не франшиза, хотя начало совпадает. var fixture = new TestDb(); await SeedAsync(fixture, Movie("Человек дождя", 1988), Movie("Человек-слон", 1980)); var suggestions = await BuildAsync(fixture); Assert.DoesNotContain(suggestions, s => s.Source == CollectionSuggestionSource.NameGuess); } /// /// Часть, приехавшая после сборки коллекции, сама франшизы не образует — её предлагают /// добавить в существующую. Иначе она не показывалась бы нигде: создавать вторую франшизу /// с тем же именем нельзя. /// [Fact] public async Task SuggestsAddition_ToExistingCollection_ByMetadata() { var fixture = new TestDb(); var first = Movie("Рокки", 1976, franchise: "1241"); var second = Movie("Рокки 2", 1979, franchise: "1241"); var collection = Collection.Create("Рокки"); collection.AddShow(first.Id); await SeedWithCollectionAsync(fixture, collection, first, second); var suggestion = Assert.Single(await BuildAsync(fixture)); Assert.Equal(collection.Id, suggestion.CollectionId); Assert.Equal(CollectionSuggestionSource.Metadata, suggestion.Source); Assert.Equal("Рокки", suggestion.Name); // В дополнении только недостающее: то, что уже в коллекции, предлагать нечего. Assert.Equal([second.Id], suggestion.Parts.Select(p => p.ShowId)); } /// Без метаданных дополнение опирается на догадку — и подписано как догадка. [Fact] public async Task SuggestsAddition_ToExistingCollection_ByName() { var fixture = new TestDb(); var first = Movie("Рокки", 1976); var second = Movie("Рокки 2", 1979); var collection = Collection.Create("Рокки"); collection.AddShow(first.Id); await SeedWithCollectionAsync(fixture, collection, first, second); var suggestion = Assert.Single(await BuildAsync(fixture)); Assert.Equal(collection.Id, suggestion.CollectionId); Assert.Equal(CollectionSuggestionSource.NameGuess, suggestion.Source); Assert.Equal([second.Id], suggestion.Parts.Select(p => p.ShowId)); } /// Чужой фильм в существующую франшизу не уезжает — правила те же, что при сборке. [Fact] public async Task DoesNotSuggestAddition_ForUnrelatedMovie() { var fixture = new TestDb(); var member = Movie("Рокки", 1976, franchise: "1241"); var stranger = Movie("Человек дождя", 1988, franchise: "777"); var collection = Collection.Create("Рокки"); collection.AddShow(member.Id); await SeedWithCollectionAsync(fixture, collection, member, stranger); Assert.Empty(await BuildAsync(fixture)); } /// /// Фильм, попавший в дополнение, из предложений «создать» уходит: два предложения на один /// фильм заставили бы выбирать вслепую, а создание к тому же упёрлось бы в занятое имя. /// [Fact] public async Task AdditionWins_OverNewFranchise() { var fixture = new TestDb(); var member = Movie("Рокки", 1976, franchise: "1241"); var second = Movie("Рокки 2", 1979, franchise: "1241"); var third = Movie("Рокки 3", 1982, franchise: "1241"); var collection = Collection.Create("Рокки"); collection.AddShow(member.Id); await SeedWithCollectionAsync(fixture, collection, member, second, third); var suggestion = Assert.Single(await BuildAsync(fixture)); Assert.Equal(collection.Id, suggestion.CollectionId); Assert.Equal([second.Id, third.Id], suggestion.Parts.Select(p => p.ShowId)); } [Fact] public async Task AddSuggestedShows_AppendsToCollection() { var fixture = new TestDb(); var first = Movie("Рокки", 1976, franchise: "1241"); var second = Movie("Рокки 2", 1979, franchise: "1241"); var collection = Collection.Create("Рокки"); collection.AddShow(first.Id); await SeedWithCollectionAsync(fixture, collection, first, second); var key = (await BuildAsync(fixture)).Single().Key; await using (var db = fixture.New()) { var result = await new AddSuggestedShowsCommandHandler( db, new CollectionSuggestionBuilder(db), new CollectionPosterPicker(db) ).Handle(new AddSuggestedShowsCommand(key), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(1, result.Value); await db.SaveChangesAsync(CancellationToken.None); } await using var check = fixture.New(); var stored = check.Collections.Include(c => c.Items).Single(); // Дополнение встаёт в конец в хронологии — франшиза остаётся в порядке просмотра. Assert.Equal( [first.Id, second.Id], stored.Items.OrderBy(i => i.Position).Select(i => i.ShowId) ); // Предложение исчерпано: добавлять больше нечего. Assert.Empty(await BuildAsync(fixture)); } /// Ключ предложения «создать» дополнением не применяется — коллекции у него нет. [Fact] public async Task AddSuggestedShows_RejectsCreationKey() { var fixture = new TestDb(); await SeedAsync(fixture, Movie("Рокки", 1976), Movie("Рокки 2", 1979)); var key = (await BuildAsync(fixture)).Single().Key; await using var db = fixture.New(); var result = await new AddSuggestedShowsCommandHandler( db, new CollectionSuggestionBuilder(db), new CollectionPosterPicker(db) ).Handle(new AddSuggestedShowsCommand(key), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(CollectionErrors.SuggestionNotFound.Code, result.Error.Code); } [Fact] public async Task AddShowsToCollection_CreatesNewAndOrdersByYear() { var fixture = new TestDb(); var later = Movie("Вторая", 1979); var earlier = Movie("Первая", 1976); await SeedAsync(fixture, later, earlier); await using var db = fixture.New(); var result = await new AddShowsToCollectionCommandHandler(db).Handle( new AddShowsToCollectionCommand([later.Id, earlier.Id], NewCollectionName: "Франшиза"), CancellationToken.None ); Assert.True(result.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); await using var check = fixture.New(); var collection = check.Collections.Include(c => c.Items).Single(); Assert.Equal(2, result.Value.Added); Assert.Equal( [earlier.Id, later.Id], collection.Items.OrderBy(i => i.Position).Select(i => i.ShowId) ); } private static Show Movie(string name, int? year, string? franchise = null) { var show = Show.Create(name, ShowKind.Single); if (franchise is not null) show.ApplyMetadata( "tmdb", Guid.NewGuid().ToString("N"), null, year, null, new MetadataFranchise(franchise, $"Франшиза {franchise}") ); else show.ApplyMetadata("manual", Guid.NewGuid().ToString("N"), null, year, null); return show; } private static async Task SeedAsync(TestDb fixture, params Show[] shows) { await using var seed = fixture.New(); seed.Shows.AddRange(shows); await seed.SaveChangesAsync(CancellationToken.None); } private static async Task SeedWithCollectionAsync( TestDb fixture, Collection collection, params Show[] shows ) { await using var seed = fixture.New(); seed.Shows.AddRange(shows); seed.Collections.Add(collection); await seed.SaveChangesAsync(CancellationToken.None); } private static async Task> BuildAsync(TestDb fixture) { await using var db = fixture.New(); return await new CollectionSuggestionBuilder(db).BuildAsync(CancellationToken.None); } }