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