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 MovieImportHandlerTests
{
    private const string Provider = "tmdb";

    private static IMetadataProviderResolver EmptyResolver()
    {
        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.AvailableKeys.Returns([]);
        // Явный null: без него NSubstitute подсунул бы автозаглушку провайдера, и «источник
        // не настроен» перестало бы быть тем, что проверяет тест.
        resolver.Resolve(Arg.Any<string>()).Returns((IMetadataProvider?)null);
        return resolver;
    }

    private static ShowMetadataApplier Applier(
        IAppDbContext db,
        IMetadataProviderResolver resolver
    ) =>
        new(
            db,
            resolver,
            Substitute.For<IImageDownloader>(),
            Substitute.For<IImageStore>(),
            new GenreMatcher(db),
            NullLogger<ShowMetadataApplier>.Instance
        );

    [Fact]
    public async Task Match_ReturnsNothing_WhenProviderIsNotConfigured()
    {
        // Ключа источника нет — искать нечем, и это не ошибка: таблица разбора просто пуста.
        var fixture = new TestDb();
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, EmptyResolver());

        var rows = await new MatchMoviesQueryHandler(matcher).Handle(
            new MatchMoviesQuery(["Gladiator.2000.mkv"], Provider),
            CancellationToken.None
        );

        Assert.Empty(rows);
    }

    [Fact]
    public async Task MatchOne_ReturnsNotFound_WhenProviderIsNotConfigured()
    {
        var fixture = new TestDb();
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, EmptyResolver());

        var row = await new MatchMovieQueryHandler(matcher).Handle(
            new MatchMovieQuery("file.mkv", "Гладиатор", 2000, Provider),
            CancellationToken.None
        );

        Assert.Equal(MovieMatchStatus.NotFound, row.Status);
        Assert.Equal("file.mkv", row.Name);
    }

    [Fact]
    public async Task Import_ReportsMissingAsset()
    {
        var fixture = new TestDb();
        await using var db = fixture.New();
        var storage = Substitute.For<IMediaStorage>();
        storage.ListManualInbox(Arg.Any<int>()).Returns([]);

        var result = await new ImportMoviesCommandHandler(
            db,
            new ManualInboxTaker(db, storage),
            Substitute.For<IMediaProcessingQueue>(),
            Applier(db, EmptyResolver())
        ).Handle(
            new ImportMoviesCommand(
                [new MovieImportItem(null, Guid.NewGuid(), "Пропавший", null, null)],
                Provider
            ),
            CancellationToken.None
        );

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

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

        await using var db = fixture.New();
        var resolver = EmptyResolver();
        var result = await new AutoAttachMovieCommandHandler(
            db,
            resolver,
            new MovieMatcher(db, resolver),
            Applier(db, resolver)
        ).Handle(new AutoAttachMovieCommand(asset.Id, "a.mkv"), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.False(result.Value);
    }

    [Fact]
    public async Task AutoAttach_FailsOnMissingAsset()
    {
        var fixture = new TestDb();
        await using var db = fixture.New();

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

        var result = await new AutoAttachMovieCommandHandler(
            db,
            resolver,
            new MovieMatcher(db, resolver),
            Applier(db, resolver)
        ).Handle(new AutoAttachMovieCommand(Guid.NewGuid(), "a.mkv"), CancellationToken.None);

        Assert.False(result.IsSuccess);
    }

    [Fact]
    public async Task AutoAttach_SkipsAssetAlreadyInShow()
    {
        // Ассет мог уехать в шоу вручную, пока файл ждал очереди, — второе шоу по нему не заводим.
        var fixture = new TestDb();
        var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Inbox);
        var show = Show.Create("Уже есть", ShowKind.Single);
        show.AddEpisode(asset.Id);

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

        await using var db = fixture.New();
        var provider = Substitute.For<IMetadataProvider>();
        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.AvailableKeys.Returns([Provider]);
        resolver.Resolve(Provider).Returns(provider);

        var result = await new AutoAttachMovieCommandHandler(
            db,
            resolver,
            new MovieMatcher(db, resolver),
            Applier(db, resolver)
        ).Handle(new AutoAttachMovieCommand(asset.Id, "a.mkv"), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.False(result.Value);
        await provider
            .DidNotReceive()
            .SearchShowsAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>());
    }
}
