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;

/// <summary>
/// Сборка франшиз: предложения по метаданным и по названиям плюс массовое добавление в коллекцию.
/// Ошибка здесь означает чужой фильм в середине франшизы, поэтому осторожность важнее полноты.
/// </summary>
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);
    }

    /// <summary>
    /// Часть, приехавшая после сборки коллекции, сама франшизы не образует — её предлагают
    /// добавить в существующую. Иначе она не показывалась бы нигде: создавать вторую франшизу
    /// с тем же именем нельзя.
    /// </summary>
    [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));
    }

    /// <summary>Без метаданных дополнение опирается на догадку — и подписано как догадка.</summary>
    [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));
    }

    /// <summary>Чужой фильм в существующую франшизу не уезжает — правила те же, что при сборке.</summary>
    [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));
    }

    /// <summary>
    /// Фильм, попавший в дополнение, из предложений «создать» уходит: два предложения на один
    /// фильм заставили бы выбирать вслепую, а создание к тому же упёрлось бы в занятое имя.
    /// </summary>
    [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));
    }

    /// <summary>Ключ предложения «создать» дополнением не применяется — коллекции у него нет.</summary>
    [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<IReadOnlyList<CollectionSuggestion>> BuildAsync(TestDb fixture)
    {
        await using var db = fixture.New();
        return await new CollectionSuggestionBuilder(db).BuildAsync(CancellationToken.None);
    }
}
