using TeleWave.Application.Streaming.GetIptvGuide;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using Xunit;
namespace TeleWave.Application.Tests.Streaming;
///
/// Телегид для IPTV: программа всех включённых каналов одной выгрузкой. Проверяем то, чем он
/// отличается от списка записей ленты — склейку серий в блок, отбор окна и подстановку описаний.
///
public class IptvGuideTests
{
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 12, 0, 0, TimeSpan.Zero);
[Fact]
public async Task Guide_MergesConsecutiveEpisodesOfTheSameShow()
{
var fixture = new TestDb();
var channel = Enabled("Первый", "one");
var show = Show.Create("Футурама", ShowKind.Series);
var first = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload);
var second = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload);
show.AddEpisode(first.Id);
show.AddEpisode(second.Id);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Shows.Add(show);
seed.MediaAssets.AddRange(first, second);
seed.ScheduleEntries.AddRange(
Program(channel.Id, first.Id, show.Id, T0, 25),
Program(channel.Id, second.Id, show.Id, T0.AddMinutes(25), 25)
);
await seed.SaveChangesAsync(CancellationToken.None);
}
var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3));
// Две серии встык — одна передача в глазах зрителя: гид показывает программу, а не файлы.
var programme = Assert.Single(Assert.Single(channels).Programmes);
Assert.Equal("Футурама", programme.Title);
Assert.Equal(T0, programme.StartsAtUtc);
Assert.Equal(T0.AddMinutes(50), programme.EndsAtUtc);
}
[Fact]
public async Task Guide_KeepsBlocksApartWhenSomethingRanBetween()
{
var fixture = new TestDb();
var channel = Enabled("Первый", "one");
var show = Show.Create("Футурама", ShowKind.Series);
var first = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload);
var second = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload);
show.AddEpisode(first.Id);
show.AddEpisode(second.Id);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Shows.Add(show);
seed.MediaAssets.AddRange(first, second);
seed.ScheduleEntries.AddRange(
Program(channel.Id, first.Id, show.Id, T0, 25),
// Разрыв в пять минут — между сериями шла реклама, и одной передачей это не назвать.
Program(channel.Id, second.Id, show.Id, T0.AddMinutes(30), 25)
);
await seed.SaveChangesAsync(CancellationToken.None);
}
var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3));
var programmes = Assert.Single(channels).Programmes;
Assert.Equal(2, programmes.Count);
Assert.Equal(T0.AddMinutes(25), programmes[0].EndsAtUtc);
Assert.Equal(T0.AddMinutes(30), programmes[1].StartsAtUtc);
}
[Fact]
public async Task Guide_TakesTitleAndCoverFromEpisodeThenFallsBackToShow()
{
var fixture = new TestDb();
var channel = Enabled("Первый", "one");
var stillId = Guid.NewGuid();
var posterId = Guid.NewGuid();
var titled = Show.Create("Футурама", ShowKind.Series);
titled.SetPosterImage(posterId);
var withMetadata = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload);
var bare = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload);
titled
.AddEpisode(withMetadata.Id)
.ApplyMetadata("Космический пилот", "Фрай", stillId, null);
titled.AddEpisode(bare.Id);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Shows.Add(titled);
seed.MediaAssets.AddRange(withMetadata, bare);
seed.ScheduleEntries.AddRange(
Program(channel.Id, withMetadata.Id, titled.Id, T0, 25),
Program(channel.Id, bare.Id, titled.Id, T0.AddMinutes(30), 25)
);
await seed.SaveChangesAsync(CancellationToken.None);
}
var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3));
var programmes = Assert.Single(channels).Programmes;
Assert.Equal("Космический пилот", programmes[0].SubTitle);
Assert.Equal("Фрай", programmes[0].Description);
Assert.Equal(stillId, programmes[0].ImageId);
// Серия без метаданных показывается обложкой шоу — пустая карточка в плеере хуже общей.
Assert.Null(programmes[1].SubTitle);
Assert.Equal(posterId, programmes[1].ImageId);
}
[Fact]
public async Task Guide_TakesOnlyEnabledChannelsAndTheRequestedWindow()
{
var fixture = new TestDb();
var visible = Enabled("Первый", "one");
var hidden = Channel.Create("Выключенный", "off", T0);
hidden.UpdateSettings("Выключенный", isEnabled: false, null);
var show = Show.Create("Ворон", ShowKind.Single);
var asset = MediaAsset.Register("crow.mkv", ".mkv", MediaSource.Upload);
show.AddEpisode(asset.Id);
await using (var seed = fixture.New())
{
seed.Channels.AddRange(visible, hidden);
seed.Shows.Add(show);
seed.MediaAssets.Add(asset);
seed.ScheduleEntries.AddRange(
Program(visible.Id, asset.Id, show.Id, T0, 60),
// За окном — не в выгрузке; выключенный канал не показываем вовсе.
Program(visible.Id, asset.Id, show.Id, T0.AddHours(10), 60),
Program(hidden.Id, asset.Id, show.Id, T0, 60),
// Заставки и врезки в гид не идут: у них нет шоу, а значит и передачи.
ScheduleEntry.Bumper(
visible.Id,
asset.Id,
T0.AddHours(2),
T0.AddHours(2).AddSeconds(10),
null,
null
)
);
await seed.SaveChangesAsync(CancellationToken.None);
}
var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3));
var dto = Assert.Single(channels);
Assert.Equal("one", dto.Slug);
var programme = Assert.Single(dto.Programmes);
Assert.Equal("Ворон", programme.Title);
}
[Fact]
public async Task Guide_WithoutChannels_ReturnsEmpty()
{
var fixture = new TestDb();
var channels = await GuideAsync(fixture, T0, T0.AddHours(3));
Assert.Empty(channels);
}
private static async Task> GuideAsync(
TestDb fixture,
DateTimeOffset from,
DateTimeOffset to
)
{
await using var db = fixture.New();
return await new GetIptvGuideQueryHandler(db).Handle(
new GetIptvGuideQuery(from, to),
CancellationToken.None
);
}
private static Channel Enabled(string name, string slug)
{
var channel = Channel.Create(name, slug, T0);
channel.UpdateSettings(name, isEnabled: true, null);
return channel;
}
private static ScheduleEntry Program(
Guid channelId,
Guid assetId,
Guid showId,
DateTimeOffset startsAt,
int minutes
) =>
ScheduleEntry.Program(
channelId,
assetId,
startsAt,
startsAt.AddMinutes(minutes),
showId,
0
);
}