using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Media.MovieImport;
using TeleWave.Application.Metadata;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Library;
using Xunit;

namespace TeleWave.Application.Tests.Media;

/// <summary>
/// Подбор фильма по имени файла. Главное свойство то же, что у массового обогащения: уверенным
/// считается только дословное совпадение — чужой постер у фильма в эфире хуже, чем его отсутствие.
/// </summary>
public class MovieMatcherTests
{
    private static IMetadataProvider ProviderWith(params MetadataCandidate[] candidates)
    {
        var provider = Substitute.For<IMetadataProvider>();
        provider
            .SearchShowsAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>())
            .Returns(candidates);
        return provider;
    }

    private static IMetadataProviderResolver Resolver(IMetadataProvider provider)
    {
        var resolver = Substitute.For<IMetadataProviderResolver>();
        resolver.Resolve("tmdb").Returns(provider);
        resolver.AvailableKeys.Returns(["tmdb"]);
        return resolver;
    }

    [Fact]
    public async Task Confident_WhenTitleAndYearMatch()
    {
        var fixture = new TestDb();
        var provider = ProviderWith(
            new MetadataCandidate("42", "Die Hard 2", 1990, null, null, ShowKind.Single)
        );
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, Resolver(provider));

        var rows = await matcher.MatchAsync(
            ["Die Hard 2 [1990 HDRip].avi"],
            "tmdb",
            CancellationToken.None
        );

        var row = Assert.Single(rows);
        Assert.Equal(MovieMatchStatus.Confident, row.Status);
        Assert.Equal("42", row.PickedExternalId);
        Assert.Equal("Die Hard 2", row.ParsedTitle);
        Assert.Equal(1990, row.ParsedYear);
    }

    [Fact]
    public async Task Confident_WhenYearIsOffByOne()
    {
        // Год проката и год производства у релизов расходятся на год — это норма, а не промах.
        var fixture = new TestDb();
        var provider = ProviderWith(
            new MetadataCandidate("7", "The Crow", 1994, null, null, ShowKind.Single)
        );
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, Resolver(provider));

        var rows = await matcher.MatchAsync(
            ["The Crow 1993 BDRip"],
            "tmdb",
            CancellationToken.None
        );

        Assert.Equal(MovieMatchStatus.Confident, Assert.Single(rows).Status);
    }

    [Fact]
    public async Task Uncertain_WhenYearIsFarOff()
    {
        var fixture = new TestDb();
        var provider = ProviderWith(
            new MetadataCandidate("7", "The Crow", 1994, null, null, ShowKind.Single)
        );
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, Resolver(provider));

        var rows = await matcher.MatchAsync(
            ["The Crow 2024 BDRip"],
            "tmdb",
            CancellationToken.None
        );

        var row = Assert.Single(rows);
        Assert.Equal(MovieMatchStatus.Uncertain, row.Status);
        Assert.Null(row.PickedExternalId);
        // Кандидаты всё равно показываются: выбрать из них — секунда, искать заново — минута.
        Assert.NotEmpty(row.Candidates);
    }

    [Fact]
    public async Task Uncertain_WhenTwoCandidatesShareTitleAndYear()
    {
        var fixture = new TestDb();
        var provider = ProviderWith(
            new MetadataCandidate("1", "Gladiator", 2000, null, null, ShowKind.Single),
            new MetadataCandidate("2", "Gladiator", 2000, null, null, ShowKind.Single)
        );
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, Resolver(provider));

        var rows = await matcher.MatchAsync(
            ["Gladiator.2000.BDRip"],
            "tmdb",
            CancellationToken.None
        );

        Assert.Equal(MovieMatchStatus.Uncertain, Assert.Single(rows).Status);
    }

    [Fact]
    public async Task NotFound_WhenProviderReturnsNothing()
    {
        // Транслит: источник по «Shou Trumana» не найдёт ничего, и это отдельный статус — строку
        // надо не выбрать, а переписать.
        var fixture = new TestDb();
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, Resolver(ProviderWith()));

        var rows = await matcher.MatchAsync(
            ["Shou.Trumana.1998.BDRip.AVC.mkv"],
            "tmdb",
            CancellationToken.None
        );

        var row = Assert.Single(rows);
        Assert.Equal(MovieMatchStatus.NotFound, row.Status);
        Assert.Empty(row.Candidates);
    }

    [Fact]
    public async Task AlreadyInLibrary_WhenExternalIdIsTaken()
    {
        var fixture = new TestDb();
        var existing = Show.Create("Гладиатор", ShowKind.Single);
        existing.ApplyMetadata("tmdb", "98", null, 2000, null);

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

        var provider = ProviderWith(
            new MetadataCandidate("98", "Gladiator", 2000, null, null, ShowKind.Single)
        );
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, Resolver(provider));

        var rows = await matcher.MatchAsync(
            ["Gladiator.2000.BDRip"],
            "tmdb",
            CancellationToken.None
        );

        var row = Assert.Single(rows);
        Assert.Equal(MovieMatchStatus.AlreadyInLibrary, row.Status);
        Assert.Equal(existing.Id, row.ExistingShowId);
        Assert.Equal("Гладиатор", row.ExistingShowName);
    }

    [Fact]
    public async Task MatchQuery_UsesTitleTypedByHand()
    {
        // Транслит правится руками: имя файла остаётся ключом строки, а ищется вписанное название.
        var fixture = new TestDb();
        var provider = ProviderWith(
            new MetadataCandidate("5", "Шоу Трумана", 1998, null, null, ShowKind.Single)
        );
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, Resolver(provider));

        var row = await matcher.MatchQueryAsync(
            "Shou.Trumana.1998.BDRip.AVC.mkv",
            "Шоу Трумана",
            1998,
            "tmdb",
            CancellationToken.None
        );

        Assert.Equal("Shou.Trumana.1998.BDRip.AVC.mkv", row.Name);
        Assert.Equal(MovieMatchStatus.Confident, row.Status);
        Assert.Equal("5", row.PickedExternalId);
    }

    [Fact]
    public async Task PickConfident_ReturnsNothing_WhenMatchIsLoose()
    {
        // Автоматика inbox обязана молчать там, где таблица разбора показала бы жёлтым.
        var fixture = new TestDb();
        var provider = ProviderWith(
            new MetadataCandidate("3", "Совсем другое кино", 1999, null, null, ShowKind.Single)
        );
        await using var db = fixture.New();
        var matcher = new MovieMatcher(db, Resolver(provider));

        var picked = await matcher.PickConfidentAsync(
            "Green.Mile.1999.bdrip.mkv",
            "tmdb",
            CancellationToken.None
        );

        Assert.Null(picked);
    }
}
