using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Media.ManualInbox;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Infrastructure.Persistence;
using Xunit;

namespace TeleWave.Integration.Tests;

/// <summary>
/// Ручной разбор <c>manual/</c>: выбранные файлы уходят из каталога в шоу. Проверяется против
/// настоящей БД, потому что вся суть операции — в связке «ассет + серия шоу + файл на диске».
/// </summary>
[Collection("postgres")]
public sealed class ManualInboxIntegrationTests(PostgresFixture fixture)
{
    [SkippableFact]
    public async Task Import_AddsEpisodesToShow_AndConsumesFiles()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");

        await using var seedDb = fixture.CreateContext();
        var showId = await SeedShowAsync(seedDb);
        var tag = Guid.NewGuid().ToString("N")[..8];
        var first = $"{tag}-s01e01.mkv";
        var second = $"{tag}-s01e02.mkv";

        var storage = Substitute.For<IMediaStorage>();
        storage
            .ListManualInbox(Arg.Any<int>())
            .Returns([
                new IMediaStorage.ManualInboxFile($"Сериал/{first}", first, 1000),
                new IMediaStorage.ManualInboxFile($"Сериал/{second}", second, 1000),
            ]);

        await using var db = fixture.CreateContext();
        var result = await new ImportManualInboxCommandHandler(
            db,
            new ManualInboxTaker(db, storage),
            Substitute.For<IMediaProcessingQueue>()
        ).Handle(
            new ImportManualInboxCommand(
                [Item($"Сериал/{first}"), Item($"Сериал/{second}")],
                showId
            ),
            default
        );

        Assert.True(result.IsSuccess);
        Assert.Equal(2, result.Value.Imported);
        Assert.Empty(result.Value.Failed);
        await db.SaveChangesAsync();

        // Файлы должны быть перенесены из manual/ — ровно как из обычного inbox.
        await storage
            .Received(2)
            .PromoteToOriginalAsync(
                MediaSource.ManualInbox,
                Arg.Any<string>(),
                Arg.Any<Guid>(),
                ".mkv",
                Arg.Any<CancellationToken>()
            );

        await using var verify = fixture.CreateContext();
        var episodes = verify
            .Shows.Where(s => s.Id == showId)
            .SelectMany(s => s.Episodes)
            .OrderBy(e => e.Position)
            .ToList();

        Assert.Equal(2, episodes.Count);
        // Номера сезона и серии разбираются из имени файла, как при обычном добавлении.
        Assert.Equal(1, episodes[0].Season);
        Assert.Equal(1, episodes[0].Episode);
        Assert.Equal(2, episodes[1].Episode);

        // Считаем ассеты своего шоу: база в интеграционных тестах общая, и соседний тест тоже
        // заводит записи из ручного inbox.
        var assetIds = episodes.Select(e => e.MediaAssetId).ToList();
        var assets = verify.MediaAssets.Where(a => assetIds.Contains(a.Id)).ToList();
        Assert.Equal(2, assets.Count);
        Assert.All(assets, a => Assert.Equal(MediaSource.ManualInbox, a.Source));
        Assert.All(assets, a => Assert.Equal(MediaAssetStatus.Pending, a.Status));
    }

    [SkippableFact]
    public async Task Import_SkipsUnsupportedAndMissing_WithoutFailingTheBatch()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");

        await using var seedDb = fixture.CreateContext();
        var showId = await SeedShowAsync(seedDb);

        var tag = Guid.NewGuid().ToString("N")[..8];
        var good = $"{tag}-ok.mkv";

        var storage = Substitute.For<IMediaStorage>();
        storage
            .ListManualInbox(Arg.Any<int>())
            .Returns([
                new IMediaStorage.ManualInboxFile(good, good, 1000),
                new IMediaStorage.ManualInboxFile("readme.txt", "readme.txt", 10),
            ]);

        await using var db = fixture.CreateContext();
        var result = await new ImportManualInboxCommandHandler(
            db,
            new ManualInboxTaker(db, storage),
            Substitute.For<IMediaProcessingQueue>()
        ).Handle(
            new ImportManualInboxCommand(
                [Item(good), Item("readme.txt"), Item("ушёл.mkv")],
                showId
            ),
            default
        );

        // Один взяли, два отклонили — и каждый со своей причиной, а не одной ошибкой на пакет.
        Assert.True(result.IsSuccess);
        Assert.Equal(1, result.Value.Imported);
        Assert.Equal(2, result.Value.Failed.Count);
        Assert.Contains(result.Value.Failed, f => f.RelativePath == "readme.txt");
        Assert.Contains(result.Value.Failed, f => f.RelativePath == "ушёл.mkv");
    }

    [SkippableFact]
    public async Task Import_WithExplicitNumbers_UsesThemInsteadOfFileName()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");

        await using var seedDb = fixture.CreateContext();
        var showId = await SeedShowAsync(seedDb);
        var name = $"{Guid.NewGuid():N}"[..8] + "-без-номеров.mkv";

        var storage = Substitute.For<IMediaStorage>();
        storage
            .ListManualInbox(Arg.Any<int>())
            .Returns([new IMediaStorage.ManualInboxFile(name, name, 1000)]);

        await using var db = fixture.CreateContext();
        var result = await new ImportManualInboxCommandHandler(
            db,
            new ManualInboxTaker(db, storage),
            Substitute.For<IMediaProcessingQueue>()
        ).Handle(
            new ImportManualInboxCommand([new ManualImportItem(name, 4, 12)], showId),
            default
        );

        Assert.True(result.IsSuccess);
        await db.SaveChangesAsync();

        await using var verify = fixture.CreateContext();
        var episode = verify.Shows.Where(s => s.Id == showId).SelectMany(s => s.Episodes).Single();

        // Из имени номера не вытащить — значит сохранилось ровно то, что видел пользователь.
        Assert.Equal(4, episode.Season);
        Assert.Equal(12, episode.Episode);

        // И спутники рядом с забранным файлом убираются.
        await storage.Received(1).CleanupManualLeftoversAsync(name, Arg.Any<CancellationToken>());
    }

    /// <summary>Файл без явных номеров — сервер разбирает имя сам, как при обычном добавлении.</summary>
    private static ManualImportItem Item(string relativePath) => new(relativePath, null, null);

    private static async Task<Guid> SeedShowAsync(AppDbContext db)
    {
        var show = Show.Create($"Сериал {Guid.NewGuid():N}"[..20], ShowKind.Series);
        db.Shows.Add(show);
        await db.SaveChangesAsync();
        return show.Id;
    }
}
