using Microsoft.EntityFrameworkCore;
using NSubstitute;
using TeleWave.Application.Broadcast.Bumpers;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Images;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Images;
using TeleWave.Domain.Programming;
using Xunit;
namespace TeleWave.Application.Tests.Broadcast;
///
/// Команды блока заставки. Блок общий для всех каналов, поэтому удаление используемого запрещено:
/// иначе оно молча выключило бы заставки в чужих каналах.
///
public class BumperTemplateHandlersTests
{
private static async Task<(TestDb Fixture, BumperTemplate Template)> SeedAsync()
{
var fixture = new TestDb();
var template = BumperTemplate.Create("Блок", "Текст 1");
await using var seed = fixture.New();
seed.BumperTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
return (fixture, template);
}
[Fact]
public async Task Create_StartsFromNowNextPreset()
{
var fixture = new TestDb();
await using var db = fixture.New();
var result = await new CreateBumperTemplateCommandHandler(db).Handle(
new CreateBumperTemplateCommand("Ночной"),
CancellationToken.None
);
await db.SaveChangesAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
await using var verify = fixture.New();
var template = await verify.BumperTemplates.Include(t => t.Variants).SingleAsync();
var variant = Assert.Single(template.Variants);
// Пустой блок в редакторе выглядит поломанным — новый начинается с «Сейчас/Далее».
Assert.Equal(
["СЕЙЧАС", "{now.title}", "ДАЛЕЕ", "{next.title}"],
variant.Lines.OrderBy(l => l.Position).Select(l => l.Text)
);
Assert.Equal(BumperFont.Sans, template.Font);
}
[Fact]
public async Task UpdateStyle_BumpsRevision()
{
var (fixture, template) = await SeedAsync();
await using var db = fixture.New();
var result = await new UpdateBumperTemplateCommandHandler(db).Handle(
new UpdateBumperTemplateCommand(
template.Id,
new BumperStyle(
"Ночной",
BumperFont.Serif,
"0x111111",
"0x222222",
"0x333333",
"black"
)
),
CancellationToken.None
);
await db.SaveChangesAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
await using var verify = fixture.New();
var stored = await verify.BumperTemplates.AsNoTracking().SingleAsync();
Assert.Equal("Ночной", stored.Name);
Assert.Equal(BumperFont.Serif, stored.Font);
// Оформление входит в сигнатуру рендера: без роста ревизии в эфире остались бы старые кадры.
Assert.Equal(1, stored.Revision);
}
[Fact]
public async Task UpdateStyle_UnknownTemplate_ReturnsNotFound()
{
var fixture = new TestDb();
await using var db = fixture.New();
var result = await new UpdateBumperTemplateCommandHandler(db).Handle(
new UpdateBumperTemplateCommand(
Guid.NewGuid(),
new BumperStyle("x", BumperFont.Sans, "a", "b", "c", "d")
),
CancellationToken.None
);
Assert.Equal(BumperErrors.TemplateNotFound, result.Error);
}
[Fact]
public async Task Audio_SetAndClear_TouchStorageAndDuration()
{
var (fixture, template) = await SeedAsync();
var storage = Substitute.For();
await using (var db = fixture.New())
{
var set = await new SetBumperTemplateAudioCommandHandler(db).Handle(
new SetBumperTemplateAudioCommand(template.Id, ".mp3", 12.5),
CancellationToken.None
);
Assert.True(set.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
}
await using (var verify = fixture.New())
{
var stored = await verify.BumperTemplates.AsNoTracking().SingleAsync();
Assert.Equal(".mp3", stored.AudioExtension);
Assert.Equal(12.5, stored.AudioDurationSeconds);
}
await using (var db = fixture.New())
{
var cleared = await new ClearBumperTemplateAudioCommandHandler(db, storage).Handle(
new ClearBumperTemplateAudioCommand(template.Id),
CancellationToken.None
);
Assert.True(cleared.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
}
// Файл на диске должен уйти вместе с отметкой в базе.
await storage.Received(1).DeleteAudioAsync(template.Id, Arg.Any());
await using var final = fixture.New();
Assert.Null((await final.BumperTemplates.AsNoTracking().SingleAsync()).AudioExtension);
}
[Fact]
public async Task Audio_UnknownTemplate_ReturnsNotFound()
{
var fixture = new TestDb();
await using var db = fixture.New();
var storage = Substitute.For();
var set = await new SetBumperTemplateAudioCommandHandler(db).Handle(
new SetBumperTemplateAudioCommand(Guid.NewGuid(), ".mp3", 1),
CancellationToken.None
);
var cleared = await new ClearBumperTemplateAudioCommandHandler(db, storage).Handle(
new ClearBumperTemplateAudioCommand(Guid.NewGuid()),
CancellationToken.None
);
Assert.Equal(BumperErrors.TemplateNotFound, set.Error);
Assert.Equal(BumperErrors.TemplateNotFound, cleared.Error);
}
[Fact]
public async Task Background_SetAndClear_UseImageRegistry()
{
var (fixture, template) = await SeedAsync();
var image = Image.Create(ImageCategory.BumperBackground, ".png", "bg.png");
await using (var seed = fixture.New())
{
seed.Images.Add(image);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using (var db = fixture.New())
{
var unknownImage = await new SetBumperTemplateBackgroundCommandHandler(db).Handle(
new SetBumperTemplateBackgroundCommand(template.Id, Guid.NewGuid()),
CancellationToken.None
);
Assert.Equal(ImageErrors.NotFound, unknownImage.Error);
var set = await new SetBumperTemplateBackgroundCommandHandler(db).Handle(
new SetBumperTemplateBackgroundCommand(template.Id, image.Id),
CancellationToken.None
);
Assert.True(set.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
}
await using (var verify = fixture.New())
Assert.Equal(
image.Id,
(await verify.BumperTemplates.AsNoTracking().SingleAsync()).BackgroundImageId
);
await using (var db = fixture.New())
{
var cleared = await new ClearBumperTemplateBackgroundCommandHandler(db).Handle(
new ClearBumperTemplateBackgroundCommand(template.Id),
CancellationToken.None
);
Assert.True(cleared.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
}
await using var final = fixture.New();
Assert.Null((await final.BumperTemplates.AsNoTracking().SingleAsync()).BackgroundImageId);
}
[Fact]
public async Task Background_UnknownTemplate_ReturnsNotFound()
{
var fixture = new TestDb();
await using var db = fixture.New();
var set = await new SetBumperTemplateBackgroundCommandHandler(db).Handle(
new SetBumperTemplateBackgroundCommand(Guid.NewGuid(), Guid.NewGuid()),
CancellationToken.None
);
var cleared = await new ClearBumperTemplateBackgroundCommandHandler(db).Handle(
new ClearBumperTemplateBackgroundCommand(Guid.NewGuid()),
CancellationToken.None
);
Assert.Equal(BumperErrors.TemplateNotFound, set.Error);
Assert.Equal(BumperErrors.TemplateNotFound, cleared.Error);
}
[Fact]
public async Task Delete_RefusesUsedTemplate_AndRemovesFreeOne()
{
var (fixture, template) = await SeedAsync();
var storage = Substitute.For();
var junction = JunctionTemplate.Create("Прайм");
await using (var seed = fixture.New())
{
junction
.AddElement(JunctionElementKind.Bumper)
.Update(
new JunctionElementSettings(
JunctionElementKind.Bumper,
null,
template.Id,
null,
JunctionAmountMode.Count,
1,
false,
null,
1,
null
)
);
seed.JunctionTemplates.Add(junction);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using (var db = fixture.New())
{
var used = await new DeleteBumperTemplateCommandHandler(db, storage).Handle(
new DeleteBumperTemplateCommand(template.Id),
CancellationToken.None
);
Assert.Equal(BumperErrors.TemplateInUse, used.Error);
}
await using (var db = fixture.New())
{
var stored = await db
.JunctionTemplates.Include(j => j.Elements)
.SingleAsync(j => j.Id == junction.Id);
stored.RemoveElement(stored.Elements[0].Id);
await db.SaveChangesAsync(CancellationToken.None);
}
await using (var db = fixture.New())
{
var removed = await new DeleteBumperTemplateCommandHandler(db, storage).Handle(
new DeleteBumperTemplateCommand(template.Id),
CancellationToken.None
);
Assert.True(removed.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
}
await storage.Received(1).DeleteAudioAsync(template.Id, Arg.Any());
await using var final = fixture.New();
Assert.Empty(final.BumperTemplates);
}
[Fact]
public async Task Delete_UnknownTemplate_ReturnsNotFound()
{
var fixture = new TestDb();
await using var db = fixture.New();
var result = await new DeleteBumperTemplateCommandHandler(
db,
Substitute.For()
).Handle(new DeleteBumperTemplateCommand(Guid.NewGuid()), CancellationToken.None);
Assert.Equal(BumperErrors.TemplateNotFound, result.Error);
}
[Fact]
public async Task List_MapsTemplatesWithUsageCount()
{
var fixture = new TestDb();
var used = BumperTemplate.Create("Анонсовый", "Текст 1");
used.UpdateStyle(
new BumperStyle(
"Анонсовый",
BumperFont.Serif,
"0x111111",
"0x222222",
"0x333333",
"white"
)
);
used.SetAudio(".mp3", 9);
used.Variants[0]
.SetLines([
BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
]);
var free = BumperTemplate.Create("Блок", "Текст 1");
var junction = JunctionTemplate.Create("Прайм");
foreach (var _ in Enumerable.Range(0, 2))
junction
.AddElement(JunctionElementKind.Bumper)
.Update(
new JunctionElementSettings(
JunctionElementKind.Bumper,
null,
used.Id,
null,
JunctionAmountMode.Count,
1,
false,
null,
1,
null
)
);
await using (var seed = fixture.New())
{
seed.BumperTemplates.AddRange(used, free);
seed.JunctionTemplates.Add(junction);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var templates = await new ListBumperTemplatesQueryHandler(db).Handle(
new ListBumperTemplatesQuery(),
CancellationToken.None
);
// Сортировка по имени: «Анонсовый» раньше «Блока».
Assert.Equal(["Анонсовый", "Блок"], templates.Select(t => t.Name));
var dto = templates[0];
Assert.Equal(2, dto.UsageCount);
Assert.Equal(BumperFont.Serif, dto.Font);
Assert.True(dto.HasAudio);
Assert.Equal(9, dto.AudioDurationSeconds);
Assert.Equal("{next.title}", Assert.Single(Assert.Single(dto.Variants).Lines).Text);
Assert.Equal(0, templates[1].UsageCount);
}
}