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;
///
/// Ручной разбор manual/: выбранные файлы уходят из каталога в шоу. Проверяется против
/// настоящей БД, потому что вся суть операции — в связке «ассет + серия шоу + файл на диске».
///
[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();
storage
.ListManualInbox(Arg.Any())
.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,
storage,
Substitute.For()
).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(),
Arg.Any(),
".mkv",
Arg.Any()
);
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();
storage
.ListManualInbox(Arg.Any())
.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,
storage,
Substitute.For()
).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();
storage
.ListManualInbox(Arg.Any())
.Returns([new IMediaStorage.ManualInboxFile(name, name, 1000)]);
await using var db = fixture.CreateContext();
var result = await new ImportManualInboxCommandHandler(
db,
storage,
Substitute.For()
).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());
}
/// Файл без явных номеров — сервер разбирает имя сам, как при обычном добавлении.
private static ManualImportItem Item(string relativePath) => new(relativePath, null, null);
private static async Task SeedShowAsync(AppDbContext db)
{
var show = Show.Create($"Сериал {Guid.NewGuid():N}"[..20], ShowKind.Series);
db.Shows.Add(show);
await db.SaveChangesAsync();
return show.Id;
}
}