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

namespace TeleWave.Application.Tests.Media;

/// <summary>
/// Массовое заведение фильмов: шоу на файл, метаданные из источника, файл в очередь. Фильмы
/// разбираются независимо — одна битая строка не должна отменять остальные сорок.
/// </summary>
public class ImportMoviesTests
{
    private const string Provider = "tmdb";

    private static IMediaStorage StorageWith(params string[] files)
    {
        var storage = Substitute.For<IMediaStorage>();
        storage
            .ListManualInbox(Arg.Any<int>())
            .Returns([
                .. files.Select(f => new IMediaStorage.ManualInboxFile(
                    f,
                    Path.GetFileName(f),
                    1024
                )),
            ]);
        return storage;
    }

    private static IMetadataProviderResolver Resolver(ShowMetadata? metadata = null)
    {
        var provider = Substitute.For<IMetadataProvider>();
        provider
            .GetShowAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>())
            .Returns(metadata);

        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.Resolve(Provider).Returns(provider);
        resolver.AvailableKeys.Returns([Provider]);
        return resolver;
    }

    private static async Task<ImportMoviesResultDto> ImportAsync(
        TestDb fixture,
        IMediaStorage storage,
        IMetadataProviderResolver resolver,
        IMediaProcessingQueue queue,
        params MovieImportItem[] items
    )
    {
        await using var db = fixture.New();
        var applier = new ShowMetadataApplier(
            db,
            resolver,
            Substitute.For<IImageDownloader>(),
            Substitute.For<IImageStore>(),
            new GenreMatcher(db),
            NullLogger<ShowMetadataApplier>.Instance
        );

        var result = await new ImportMoviesCommandHandler(
            db,
            new ManualInboxTaker(db, storage),
            queue,
            applier
        ).Handle(new ImportMoviesCommand(items, Provider), CancellationToken.None);

        Assert.True(result.IsSuccess);
        await db.SaveChangesAsync(CancellationToken.None);
        return result.Value;
    }

    [Fact]
    public async Task CreatesShowPerFile_AndSendsItToProcessing()
    {
        var fixture = new TestDb();
        var storage = StorageWith("Gladiator.2000.BDRip.mkv");
        var queue = Substitute.For<IMediaProcessingQueue>();

        var result = await ImportAsync(
            fixture,
            storage,
            Resolver(new ShowMetadata("98", "Gladiator", 2000, "desc", null, [], null)),
            queue,
            new MovieImportItem("Gladiator.2000.BDRip.mkv", null, "Gladiator", 2000, "98")
        );

        Assert.Equal(1, result.Created);
        Assert.Equal(1, result.Enriched);
        Assert.Empty(result.Failed);

        await using var db = fixture.New();
        var show = await db.Shows.Include(s => s.Episodes).SingleAsync(CancellationToken.None);
        Assert.Equal(ShowKind.Single, show.Kind);
        Assert.Equal("Gladiator", show.Name);
        Assert.Equal(2000, show.Year);
        Assert.Single(show.Episodes);

        // Файл ушёл в очередь обработки, а спутники из manual/ подчищены.
        queue.Received(1).Enqueue(Arg.Any<Guid>());
        await storage
            .Received(1)
            .CleanupManualLeftoversAsync("Gladiator.2000.BDRip.mkv", Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task NamesShowAsInTheSource_AndKeepsFileTitleAsOriginal()
    {
        // В таблице разбора «Название» — строка поиска: «Die Hard 2» там стоит ровно для того,
        // чтобы найти «Крепкий орешек 2». В библиотеку должно попасть название источника,
        // а латинское — остаться оригинальным.
        var fixture = new TestDb();

        var result = await ImportAsync(
            fixture,
            StorageWith("Die Hard 2 [1990 HDRip].avi"),
            Resolver(
                new ShowMetadata(
                    "42",
                    "Крепкий орешек 2",
                    1990,
                    "desc",
                    null,
                    [],
                    null,
                    null,
                    "Die Hard 2"
                )
            ),
            Substitute.For<IMediaProcessingQueue>(),
            new MovieImportItem("Die Hard 2 [1990 HDRip].avi", null, "Die Hard 2", 1990, "42")
        );

        Assert.Equal(1, result.Enriched);

        await using var db = fixture.New();
        var show = await db.Shows.SingleAsync(CancellationToken.None);
        Assert.Equal("Крепкий орешек 2", show.Name);
        Assert.Equal("Die Hard 2", show.OriginalName);
    }

    [Fact]
    public async Task KeepsTypedTitleAsOriginal_WhenSourceHasNoOriginal()
    {
        // У OMDb оригинального названия нет — тогда оригинальным становится то, что вписали
        // в таблице: по нему потом ищутся релизы.
        var fixture = new TestDb();

        var result = await ImportAsync(
            fixture,
            StorageWith("Spaun.1997.VHSRip.avi"),
            Resolver(new ShowMetadata("5", "Спаун", 1997, null, null, [], null, null)),
            Substitute.For<IMediaProcessingQueue>(),
            new MovieImportItem("Spaun.1997.VHSRip.avi", null, "Spaun", 1997, "5")
        );

        Assert.Equal(1, result.Enriched);

        await using var db = fixture.New();
        var show = await db.Shows.SingleAsync(CancellationToken.None);
        Assert.Equal("Спаун", show.Name);
        Assert.Equal("Spaun", show.OriginalName);
    }

    [Fact]
    public async Task CreatesShowWithoutMetadata_WhenCandidateIsNotChosen()
    {
        // Строка, где источник ничего не дал: шоу с файлом уже полезно, метаданные дотянет
        // массовое обогащение.
        var fixture = new TestDb();

        var result = await ImportAsync(
            fixture,
            StorageWith("Shou.Trumana.1998.BDRip.mkv"),
            Resolver(),
            Substitute.For<IMediaProcessingQueue>(),
            new MovieImportItem("Shou.Trumana.1998.BDRip.mkv", null, "Шоу Трумана", 1998, null)
        );

        Assert.Equal(1, result.Created);
        Assert.Equal(0, result.Enriched);
        Assert.Empty(result.Failed);

        await using var db = fixture.New();
        var show = await db.Shows.SingleAsync(CancellationToken.None);
        Assert.Equal("Шоу Трумана", show.Name);
        Assert.Null(show.MetadataExternalId);
    }

    [Fact]
    public async Task AttachesAlreadyUploadedAsset_WithoutQueueingItTwice()
    {
        var fixture = new TestDb();
        var asset = MediaAsset.Register("Die Hard 2.avi", ".avi", MediaSource.Upload);
        await using (var seed = fixture.New())
        {
            seed.MediaAssets.Add(asset);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var queue = Substitute.For<IMediaProcessingQueue>();
        var result = await ImportAsync(
            fixture,
            StorageWith(),
            Resolver(),
            queue,
            new MovieImportItem(null, asset.Id, "Die Hard 2", 1990, null)
        );

        Assert.Equal(1, result.Created);
        // Загруженный файл встал в очередь ещё при загрузке — второй раз ставить нечего.
        queue.DidNotReceive().Enqueue(Arg.Any<Guid>());

        await using var db = fixture.New();
        var show = await db.Shows.Include(s => s.Episodes).SingleAsync(CancellationToken.None);
        Assert.Equal(asset.Id, Assert.Single(show.Episodes).MediaAssetId);
    }

    [Fact]
    public async Task ReportsMissingFile_AndImportsTheRest()
    {
        var fixture = new TestDb();

        var result = await ImportAsync(
            fixture,
            StorageWith("Gladiator.2000.BDRip.mkv"),
            Resolver(),
            Substitute.For<IMediaProcessingQueue>(),
            new MovieImportItem("Ушёл.из.каталога.mkv", null, "Пропавший", null, null),
            new MovieImportItem("Gladiator.2000.BDRip.mkv", null, "Gladiator", 2000, null)
        );

        Assert.Equal(1, result.Created);
        var failure = Assert.Single(result.Failed);
        Assert.Equal("Пропавший", failure.Title);
    }

    [Fact]
    public async Task RejectsFileAlreadyInLibrary()
    {
        var fixture = new TestDb();
        await using (var seed = fixture.New())
        {
            seed.MediaAssets.Add(
                MediaAsset.Register("Gladiator.2000.BDRip.mkv", ".mkv", MediaSource.Upload)
            );
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var result = await ImportAsync(
            fixture,
            StorageWith("Gladiator.2000.BDRip.mkv"),
            Resolver(),
            Substitute.For<IMediaProcessingQueue>(),
            new MovieImportItem("Gladiator.2000.BDRip.mkv", null, "Gladiator", 2000, null)
        );

        Assert.Equal(0, result.Created);
        Assert.Single(result.Failed);
    }

    [Fact]
    public async Task AutoAttach_CreatesMovie_OnConfidentMatch()
    {
        var fixture = new TestDb();
        var asset = MediaAsset.Register("Gladiator.2000.BDRip.mkv", ".mkv", MediaSource.Inbox);
        await using (var seed = fixture.New())
        {
            seed.MediaAssets.Add(asset);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var provider = Substitute.For<IMetadataProvider>();
        provider
            .SearchShowsAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>())
            .Returns([new MetadataCandidate("98", "Gladiator", 2000, null, null, ShowKind.Single)]);
        provider
            .GetShowAsync("98", Arg.Any<ShowKind>(), Arg.Any<CancellationToken>())
            .Returns(new ShowMetadata("98", "Gladiator", 2000, "desc", null, [], null));

        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.Resolve(Provider).Returns(provider);
        resolver.AvailableKeys.Returns([Provider]);

        await using (var db = fixture.New())
        {
            var applier = new ShowMetadataApplier(
                db,
                resolver,
                Substitute.For<IImageDownloader>(),
                Substitute.For<IImageStore>(),
                new GenreMatcher(db),
                NullLogger<ShowMetadataApplier>.Instance
            );
            var result = await new AutoAttachMovieCommandHandler(
                db,
                resolver,
                new MovieMatcher(db, resolver),
                applier
            ).Handle(
                new AutoAttachMovieCommand(asset.Id, "Gladiator.2000.BDRip.mkv"),
                CancellationToken.None
            );

            Assert.True(result.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var check = fixture.New();
        var show = await check.Shows.Include(s => s.Episodes).SingleAsync(CancellationToken.None);
        Assert.Equal("Gladiator", show.Name);
        Assert.Equal("98", show.MetadataExternalId);
        Assert.Equal(asset.Id, Assert.Single(show.Episodes).MediaAssetId);
    }

    [Fact]
    public async Task AutoAttach_DoesNothing_WhenMatchIsLoose()
    {
        // Файл остаётся ассетом без шоу — его разберёт человек в таблице разбора.
        var fixture = new TestDb();
        var asset = MediaAsset.Register("Voron.1994.TRIPLE.mkv", ".mkv", MediaSource.Inbox);
        await using (var seed = fixture.New())
        {
            seed.MediaAssets.Add(asset);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var provider = Substitute.For<IMetadataProvider>();
        provider
            .SearchShowsAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>())
            .Returns([new MetadataCandidate("7", "The Crow", 1994, null, null, ShowKind.Single)]);

        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.Resolve(Provider).Returns(provider);
        resolver.AvailableKeys.Returns([Provider]);

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

        var result = await new AutoAttachMovieCommandHandler(
            db,
            resolver,
            new MovieMatcher(db, resolver),
            applier
        ).Handle(
            new AutoAttachMovieCommand(asset.Id, "Voron.1994.TRIPLE.mkv"),
            CancellationToken.None
        );

        Assert.True(result.IsSuccess);
        Assert.False(result.Value);
        Assert.Empty(await db.Shows.ToListAsync(CancellationToken.None));
    }
}
