using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Library.Genres;
using TeleWave.Application.Metadata;
using TeleWave.Application.Metadata.ApplyShowMetadata;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Library;
using Xunit;

namespace TeleWave.Application.Tests.Metadata;

public class ApplyShowMetadataTests
{
    [Theory]
    [InlineData(ShowKind.Series)]
    [InlineData(ShowKind.Single)]
    public async Task PassesShowKindToProvider(ShowKind kind)
    {
        // Тип определяет, в какое пространство идентификаторов пойдёт TMDb, и берётся он у самого
        // шоу, а не из команды: иначе запросом можно было бы записать сериалу метаданные
        // одноимённого фильма.
        var arranged = await ArrangeAsync(kind);
        Respond(arranged.Provider, new ShowMetadata("42", "A", 2000, null, null));

        var result = await ApplyAsync(arranged);

        Assert.True(result);
        await arranged.Provider.Received(1).GetShowAsync("42", kind, Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task AppliesRecognizedRating()
    {
        var arranged = await ArrangeAsync(ShowKind.Single);
        Respond(arranged.Provider, new ShowMetadata("42", "A", 2000, null, null, null, "PG-13"));

        await ApplyAsync(arranged);

        Assert.Equal(ShowAudience.Pg13, await SavedAudienceAsync(arranged));
    }

    [Fact]
    public async Task KeepsRating_WhenSourceGivesNothingUsable()
    {
        // «Not Rated» — это отсутствие данных. Снять им проставленный рейтинг нельзя: обновление
        // метаданных тихо открыло бы взрослому шоу дорогу в детское время.
        var arranged = await ArrangeAsync(ShowKind.Single, ShowAudience.R);
        Respond(
            arranged.Provider,
            new ShowMetadata("42", "A", 2000, null, null, null, "Not Rated")
        );

        await ApplyAsync(arranged);

        Assert.Equal(ShowAudience.R, await SavedAudienceAsync(arranged));
    }

    [Fact]
    public async Task Fails_ForInterstitial()
    {
        var arranged = await ArrangeAsync(ShowKind.Interstitial);

        Assert.False(await ApplyAsync(arranged));
        await arranged
            .Provider.DidNotReceive()
            .GetShowAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>());
    }

    private static void Respond(IMetadataProvider provider, ShowMetadata meta) =>
        provider
            .GetShowAsync("42", Arg.Any<ShowKind>(), Arg.Any<CancellationToken>())
            .Returns(meta);

    private static async Task<bool> ApplyAsync(Arranged arranged)
    {
        await using var db = arranged.Fixture.New();
        var handler = new ApplyShowMetadataCommandHandler(
            db,
            new ShowMetadataApplier(
                db,
                arranged.Resolver,
                Substitute.For<IImageDownloader>(),
                Substitute.For<IImageStore>(),
                new GenreMatcher(db),
                NullLogger<ShowMetadataApplier>.Instance
            )
        );

        var result = await handler.Handle(
            new ApplyShowMetadataCommand(arranged.ShowId, "tmdb", "42"),
            CancellationToken.None
        );

        // В обычном пути сохраняет UnitOfWorkBehavior; при прямом вызове хендлера его нет.
        if (result.IsSuccess)
            await db.SaveChangesAsync(CancellationToken.None);
        return result.IsSuccess;
    }

    private static async Task<ShowAudience?> SavedAudienceAsync(Arranged arranged)
    {
        await using var db = arranged.Fixture.New();
        return (await db.Shows.FirstAsync(s => s.Id == arranged.ShowId)).Audience;
    }

    private sealed record Arranged(
        TestDb Fixture,
        IMetadataProvider Provider,
        IMetadataProviderResolver Resolver,
        Guid ShowId
    );

    private static async Task<Arranged> ArrangeAsync(ShowKind kind, ShowAudience? audience = null)
    {
        var fixture = new TestDb();
        var show = Show.Create("A", kind);
        show.SetAudience(audience);

        await using (var seed = fixture.New())
        {
            seed.Shows.Add(show);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var provider = Substitute.For<IMetadataProvider>();
        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.Resolve("tmdb").Returns(provider);

        return new Arranged(fixture, provider, resolver, show.Id);
    }
}
