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

namespace TeleWave.Application.Tests.Metadata;

/// <summary>
/// Довыгрузка метаданных серий: что берётся за номер серии, что происходит с кадром и когда
/// операция отказывается работать вовсе.
/// </summary>
public class RefreshEpisodesTests
{
    private static EpisodeMetadata Meta(string title, string? stillUrl = null) =>
        new(title, "Описание", stillUrl, new DateOnly(2005, 3, 20));

    private static (IMetadataProviderResolver Resolver, IMetadataProvider Provider) ProviderFor(
        string key
    )
    {
        var provider = Substitute.For<IMetadataProvider>();
        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.Resolve(key).Returns(provider);
        return (resolver, provider);
    }

    private static RefreshShowEpisodesMetadataCommandHandler Handler(
        IAppDbContext db,
        IMetadataProviderResolver resolver,
        IImageDownloader? downloader = null,
        IImageStore? store = null
    ) =>
        new(
            db,
            resolver,
            downloader ?? Substitute.For<IImageDownloader>(),
            store ?? Substitute.For<IImageStore>(),
            NullLogger<RefreshShowEpisodesMetadataCommandHandler>.Instance
        );

    /// <summary>Сериал с привязкой к источнику и одной серией на готовом ассете.</summary>
    private static (Show Show, MediaAsset Asset) Linked(string fileName, bool withNumbers)
    {
        var asset = MediaAsset.Register(fileName, ".mkv", MediaSource.Upload);
        var show = Show.Create("Симпсоны", ShowKind.Series);
        show.ApplyMetadata("tmdb", "456", null, 2005, null);
        var episode = show.AddEpisode(asset.Id);
        if (withNumbers)
            episode.SetNumbers(16, 21);
        return (show, asset);
    }

    [Fact]
    public async Task AppliesMetadata_AndSavesStillToRegistry()
    {
        var fixture = new TestDb();
        var (show, asset) = Linked("s16e21.mkv", withNumbers: true);

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

        var (resolver, provider) = ProviderFor("tmdb");
        provider
            .GetEpisodeAsync("456", 16, 21, Arg.Any<CancellationToken>())
            .Returns(Meta("The Father, the Son", "https://img/still.jpg"));

        var downloader = Substitute.For<IImageDownloader>();
        downloader
            .DownloadAsync("https://img/still.jpg", Arg.Any<CancellationToken>())
            .Returns(new DownloadedImage([1, 2, 3], ".jpg"));
        var store = Substitute.For<IImageStore>();

        await using var db = fixture.New();
        var result = await Handler(db, resolver, downloader, store)
            .Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);
        await db.SaveChangesAsync(CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(1, result.Value);

        await using var check = fixture.New();
        var stored = await check.Shows.Include(s => s.Episodes).SingleAsync(CancellationToken.None);
        var episode = stored.Episodes.Single();
        Assert.Equal("The Father, the Son", episode.Title);
        Assert.NotNull(episode.StillImageId);
        // Кадр попадает в общий реестр изображений, а не в своё файловое поле.
        Assert.Equal(1, await check.Images.CountAsync(CancellationToken.None));
        await store
            .Received(1)
            .SaveAsync(
                episode.StillImageId!.Value,
                ".jpg",
                Arg.Any<byte[]>(),
                Arg.Any<CancellationToken>()
            );
    }

    [Fact]
    public async Task TakesNumbersFromFileName_WhenNotSet()
    {
        var fixture = new TestDb();
        var (show, asset) = Linked("Simpsons.S16E21.1080p.mkv", withNumbers: false);

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

        var (resolver, provider) = ProviderFor("tmdb");
        provider
            .GetEpisodeAsync("456", 16, 21, Arg.Any<CancellationToken>())
            .Returns(Meta("Разобрано по имени"));

        await using var db = fixture.New();
        var result = await Handler(db, resolver)
            .Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);
        await db.SaveChangesAsync(CancellationToken.None);

        Assert.Equal(1, result.Value);

        await using var check = fixture.New();
        var episode = (await check.Shows.Include(s => s.Episodes).SingleAsync()).Episodes.Single();
        // Разобранные номера тут же сохраняются: второй раз имя файла разбирать незачем.
        Assert.Equal(16, episode.Season);
        Assert.Equal(21, episode.Episode);
    }

    [Fact]
    public async Task SkipsEpisode_WhenNumbersUnknownOrSourceSilent()
    {
        var fixture = new TestDb();
        var unparsable = MediaAsset.Register("случайное имя.mkv", ".mkv", MediaSource.Upload);
        var known = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload);
        var show = Show.Create("Сериал", ShowKind.Series);
        show.ApplyMetadata("tmdb", "1", null, null, null);
        show.AddEpisode(unparsable.Id);
        show.AddEpisode(known.Id);

        await using (var seed = fixture.New())
        {
            seed.MediaAssets.AddRange(unparsable, known);
            seed.Shows.Add(show);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var (resolver, provider) = ProviderFor("tmdb");
        // Источник знает не всё: на распознанную серию он отвечает пустотой.
        provider
            .GetEpisodeAsync("1", Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns((EpisodeMetadata?)null);

        await using var db = fixture.New();
        var result = await Handler(db, resolver)
            .Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(0, result.Value);
        // К нераспознанной серии источник не ходит вовсе — незачем.
        await provider
            .Received(1)
            .GetEpisodeAsync("1", Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task KeepsMetadata_WhenStillCannotBeDownloaded()
    {
        var fixture = new TestDb();
        var (show, asset) = Linked("s01e01.mkv", withNumbers: true);

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

        var (resolver, provider) = ProviderFor("tmdb");
        provider
            .GetEpisodeAsync("456", Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(Meta("Без кадра", "https://img/broken.jpg"));

        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 Handler(db, resolver, downloader)
            .Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);
        await db.SaveChangesAsync(CancellationToken.None);

        // Недоступный кадр не повод отменять текст: серия обновлена, картинки просто нет.
        Assert.Equal(1, result.Value);

        await using var check = fixture.New();
        var episode = (await check.Shows.Include(s => s.Episodes).SingleAsync()).Episodes.Single();
        Assert.Equal("Без кадра", episode.Title);
        Assert.Null(episode.StillImageId);
        Assert.Equal(0, await check.Images.CountAsync(CancellationToken.None));
    }

    [Fact]
    public async Task Fails_WhenShowIsMissingOrNotSeriesOrUnlinked()
    {
        var fixture = new TestDb();
        var movie = Show.Create("Фильм", ShowKind.Single);
        var unlinked = Show.Create("Сериал без источника", ShowKind.Series);

        await using (var seed = fixture.New())
        {
            seed.Shows.AddRange(movie, unlinked);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var (resolver, _) = ProviderFor("tmdb");

        await using var db = fixture.New();
        var handler = Handler(db, resolver);

        var missing = await handler.Handle(
            new RefreshShowEpisodesMetadataCommand(Guid.NewGuid()),
            CancellationToken.None
        );
        var notSeries = await handler.Handle(
            new RefreshShowEpisodesMetadataCommand(movie.Id),
            CancellationToken.None
        );
        var noSource = await handler.Handle(
            new RefreshShowEpisodesMetadataCommand(unlinked.Id),
            CancellationToken.None
        );

        Assert.False(missing.IsSuccess);
        Assert.Equal(MetadataErrors.EpisodesForSeriesOnly, notSeries.Error);
        Assert.Equal(MetadataErrors.NoLinkedSource, noSource.Error);
    }

    [Fact]
    public async Task Fails_WhenLinkedProviderIsNotConfigured()
    {
        var fixture = new TestDb();
        var show = Show.Create("Сериал", ShowKind.Series);
        show.ApplyMetadata("omdb", "tt1", null, null, null);

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

        var resolver = Substitute.For<IMetadataProviderResolver>();
        // Явно: NSubstitute сам подставляет заглушку вместо интерфейсного null.
        resolver.Resolve(Arg.Any<string>()).Returns((IMetadataProvider?)null);

        await using var db = fixture.New();
        var result = await Handler(db, resolver)
            .Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);

        Assert.Equal(MetadataErrors.ProviderNotAvailable, result.Error);
    }
}
