using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Library.Genres;
using TeleWave.Application.Metadata;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Images;
using TeleWave.Domain.Library;
using Xunit;

namespace TeleWave.Application.Tests.Metadata;

/// <summary>
/// Применение метаданных: постер в реестр изображений, жанры, франшиза и отказы. Общий путь для
/// точечного и массового применения — расхождение между ними отлаживать было бы нечем.
/// </summary>
public class ShowMetadataApplierTests
{
    [Fact]
    public async Task RegistersPoster_InImageRegistry()
    {
        var fixture = new TestDb();
        var show = Show.Create("Шоу", ShowKind.Single);
        await SeedAsync(fixture, show);

        var downloader = Substitute.For<IImageDownloader>();
        downloader
            .DownloadAsync("https://poster", Arg.Any<CancellationToken>())
            .Returns(new DownloadedImage(Encoding.UTF8.GetBytes("img"), ".jpg"));
        var store = Substitute.For<IImageStore>();

        await using var db = fixture.New();
        var result = await Applier(db, Meta(posterUrl: "https://poster"), downloader, store)
            .ApplyAsync(await LoadAsync(db, show.Id), "tmdb", "42", CancellationToken.None);
        Assert.True(result.IsSuccess);
        await db.SaveChangesAsync(CancellationToken.None);

        await using var check = fixture.New();
        var image = check.Images.Single();
        Assert.Equal(ImageCategory.ShowPoster, image.Category);
        Assert.Equal(image.Id, check.Shows.Single().PosterImageId);
        await store
            .Received(1)
            .SaveAsync(image.Id, ".jpg", Arg.Any<byte[]>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task KeepsPoster_WhenDownloadFails()
    {
        // Источник отдал ссылку, но картинка не скачалась — это не повод валить применение.
        var fixture = new TestDb();
        var show = Show.Create("Шоу", ShowKind.Single);
        await SeedAsync(fixture, show);

        var downloader = Substitute.For<IImageDownloader>();
        downloader
            .DownloadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns((DownloadedImage?)null);

        await using var db = fixture.New();
        var result = await Applier(db, Meta(posterUrl: "https://poster"), downloader)
            .ApplyAsync(await LoadAsync(db, show.Id), "tmdb", "42", CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Empty(db.Images);
    }

    [Fact]
    public async Task RemembersFranchise_FromSource()
    {
        var fixture = new TestDb();
        var show = Show.Create("Рокки", ShowKind.Single);
        await SeedAsync(fixture, show);

        await using var db = fixture.New();
        await Applier(db, Meta(franchise: new MetadataFranchise("1241", "Рокки")))
            .ApplyAsync(await LoadAsync(db, show.Id), "tmdb", "42", CancellationToken.None);
        await db.SaveChangesAsync(CancellationToken.None);

        await using var check = fixture.New();
        var stored = check.Shows.Single();
        Assert.Equal("1241", stored.FranchiseExternalId);
        Assert.Equal("Рокки", stored.FranchiseName);
    }

    [Fact]
    public async Task Fails_WhenProviderUnavailable()
    {
        var fixture = new TestDb();
        var show = Show.Create("Шоу", ShowKind.Single);
        await SeedAsync(fixture, show);

        await using var db = fixture.New();
        var resolver = Substitute.For<IMetadataProviderResolver>();
        var applier = new ShowMetadataApplier(
            db,
            resolver,
            Substitute.For<IImageDownloader>(),
            Substitute.For<IImageStore>(),
            new GenreMatcher(db),
            NullLogger<ShowMetadataApplier>.Instance
        );

        var result = await applier.ApplyAsync(
            await LoadAsync(db, show.Id),
            "tmdb",
            "42",
            CancellationToken.None
        );

        Assert.False(result.IsSuccess);
    }

    [Fact]
    public async Task Fails_WhenSourceKnowsNothing()
    {
        var fixture = new TestDb();
        var show = Show.Create("Шоу", ShowKind.Single);
        await SeedAsync(fixture, show);

        await using var db = fixture.New();
        var result = await Applier(db, meta: null)
            .ApplyAsync(await LoadAsync(db, show.Id), "tmdb", "42", CancellationToken.None);

        Assert.False(result.IsSuccess);
    }

    private static ShowMetadata Meta(
        string? posterUrl = null,
        MetadataFranchise? franchise = null
    ) => new("42", "Шоу", 2000, "описание", posterUrl, [], null, franchise);

    private static ShowMetadataApplier Applier(
        Infrastructure.Persistence.AppDbContext db,
        ShowMetadata? meta,
        IImageDownloader? downloader = null,
        IImageStore? store = null
    )
    {
        var provider = Substitute.For<IMetadataProvider>();
        provider
            .GetShowAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>())
            .Returns(meta);
        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.Resolve("tmdb").Returns(provider);

        return new ShowMetadataApplier(
            db,
            resolver,
            downloader ?? Substitute.For<IImageDownloader>(),
            store ?? Substitute.For<IImageStore>(),
            new GenreMatcher(db),
            NullLogger<ShowMetadataApplier>.Instance
        );
    }

    private static async Task SeedAsync(TestDb fixture, Show show)
    {
        await using var seed = fixture.New();
        seed.Shows.Add(show);
        await seed.SaveChangesAsync(CancellationToken.None);
    }

    private static async Task<Show> LoadAsync(
        Infrastructure.Persistence.AppDbContext db,
        Guid showId
    ) =>
        await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstAsync(
            db.Shows,
            s => s.Id == showId
        );
}
