using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Library;
using TeleWave.Application.Library.DeleteShow;
using TeleWave.Application.Media;
using TeleWave.Application.Media.Delete;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Groups.DeleteGroup;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using Xunit;
namespace TeleWave.Application.Tests.Library;
///
/// Удаление занятой сущности: ссылки на шоу, ассет и группу живут в таблицах без внешних ключей
/// (или с Restrict), поэтому БД такое удаление либо пропустит с висячей ссылкой, либо уронит
/// исключением. Проверяем, что до неё дело не доходит — возвращается управляемая ошибка.
///
public class DeleteGuardsTests
{
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static GroupMembershipCleaner GroupCleaner(IAppDbContext db) =>
new(db, GroupServices.Stats(db));
[Fact]
public async Task DeleteShow_InFutureSchedule_Fails()
{
var fixture = new TestDb();
var show = Show.Create("A", ShowKind.Series);
var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);
var future = DateTimeOffset.UtcNow.AddHours(1);
await using (var seed = fixture.New())
{
seed.Shows.Add(show);
seed.MediaAssets.Add(asset);
seed.ScheduleEntries.Add(
ScheduleEntry.Program(
Guid.NewGuid(),
asset.Id,
future,
future.AddMinutes(20),
show.Id,
0
)
);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
new DeleteShowCommand(show.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ShowErrors.InFutureSchedule, result.Error);
}
/// Отыгранный эфир шоу не держит: иначе его нельзя было бы удалить никогда.
[Fact]
public async Task DeleteShow_OnlyPastSchedule_Succeeds()
{
var fixture = new TestDb();
var show = Show.Create("A", ShowKind.Series);
var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);
await using (var seed = fixture.New())
{
seed.Shows.Add(show);
seed.MediaAssets.Add(asset);
seed.ScheduleEntries.Add(
ScheduleEntry.Program(Guid.NewGuid(), asset.Id, T0, T0.AddMinutes(20), show.Id, 0)
);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
new DeleteShowCommand(show.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task DeleteMediaAsset_InSchedule_Fails()
{
var fixture = new TestDb();
var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);
await using (var seed = fixture.New())
{
seed.MediaAssets.Add(asset);
seed.ScheduleEntries.Add(
ScheduleEntry.Ad(Guid.NewGuid(), asset.Id, T0, T0.AddMinutes(1))
);
await seed.SaveChangesAsync(CancellationToken.None);
}
var storage = Substitute.For();
await using var db = fixture.New();
var result = await new DeleteMediaAssetCommandHandler(db, storage).Handle(
new DeleteMediaAssetCommand(asset.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(MediaErrors.InUse, result.Error);
// Файлы не тронуты: до чистки хранилища дело не дошло.
await storage
.DidNotReceiveWithAnyArgs()
.DeleteAssetArtifactsAsync(default, default!, default);
}
[Fact]
public async Task DeleteMediaAsset_ChannelFiller_Fails()
{
var fixture = new TestDb();
var asset = MediaAsset.Register("filler.mkv", ".mkv", MediaSource.Upload);
var channel = Channel.Create("c", "c", T0);
channel.UpdateSettings(channel.Name, isEnabled: true, asset.Id);
await using (var seed = fixture.New())
{
seed.MediaAssets.Add(asset);
seed.Channels.Add(channel);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new DeleteMediaAssetCommandHandler(
db,
Substitute.For()
).Handle(new DeleteMediaAssetCommand(asset.Id), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(MediaErrors.InUse, result.Error);
}
[Fact]
public async Task DeleteGroup_UsedBySlot_Fails()
{
var fixture = new TestDb();
var group = Group.Create("G");
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
var layer = template.AddLayer("Базовый", 10);
var slot = layer.AddSlot("Блок", new TimeOnly(6, 0), 60);
slot.UpdateContent(
new SlotContent(
slot.Title,
SlotKind.Content,
group.Id,
null,
null,
SlotBlockMode.FillSlot,
1,
OverflowPolicy.ContinueNext
)
);
await using (var seed = fixture.New())
{
seed.Groups.Add(group);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new DeleteGroupCommandHandler(db).Handle(
new DeleteGroupCommand(group.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(GroupErrors.InUse, result.Error);
}
/// Запасная группа шаблона внешнего ключа не имеет — её пришлось бы ловить отдельно.
[Fact]
public async Task DeleteGroup_UsedAsTemplateFallback_Fails()
{
var fixture = new TestDb();
var group = Group.Create("G");
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
template.SetFallbackGroup(group.Id);
await using (var seed = fixture.New())
{
seed.Groups.Add(group);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new DeleteGroupCommandHandler(db).Handle(
new DeleteGroupCommand(group.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(GroupErrors.InUse, result.Error);
}
[Fact]
public async Task DeleteGroup_Unreferenced_Succeeds()
{
var fixture = new TestDb();
var group = Group.Create("G");
await using (var seed = fixture.New())
{
seed.Groups.Add(group);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new DeleteGroupCommandHandler(db).Handle(
new DeleteGroupCommand(group.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
}
}