using Microsoft.Extensions.Options;
using NSubstitute;
using TeleWave.Application.Broadcast.Bumpers;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Streaming;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Images;
using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;
using TeleWave.Infrastructure.Persistence;
using Xunit;

namespace TeleWave.Application.Tests.Broadcast;

/// <summary>
/// Превью ТВ-заставки: рендерится каждый подблок блока, длительность выравнивается по длине
/// сегмента HLS, а плейсхолдеры подставляются образцами выбранного канала — блок общий, но
/// смотреть на него надо глазами конкретного канала.
/// </summary>
public class BumperPreviewTests
{
    private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);

    private static RenderBumperPreviewCommandHandler Handler(
        AppDbContext db,
        IBumperRenderer? renderer = null,
        IBumperTemplateStorage? storage = null,
        IImageStore? imageStore = null,
        IMediaStorage? mediaStorage = null
    ) =>
        new(
            db,
            renderer ?? Substitute.For<IBumperRenderer>(),
            storage ?? Substitute.For<IBumperTemplateStorage>(),
            imageStore ?? Substitute.For<IImageStore>(),
            mediaStorage ?? Substitute.For<IMediaStorage>(),
            Options.Create(new BumperOptions()),
            Options.Create(new StreamingOptions { SegmentSeconds = 5 })
        );

    private static BumperTemplate NewTemplate()
    {
        var template = BumperTemplate.Create("Блок", "Текст 1");
        template
            .Variants[0]
            .SetLines([
                BumperLine.Create(0, BumperLineStyle.Label, BumperLineColor.Accent, "СЕЙЧАС"),
                BumperLine.Create(1, BumperLineStyle.Title, BumperLineColor.Text, "{now.title}"),
                BumperLine.Create(2, BumperLineStyle.Label, BumperLineColor.Accent, "ДАЛЕЕ"),
                BumperLine.Create(3, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
            ]);
        return template;
    }

    private static IReadOnlyList<BumperRenderSpec> Specs(IBumperRenderer renderer) =>
        renderer
            .ReceivedCalls()
            .Select(c => c.GetArguments()[1])
            .OfType<BumperRenderSpec>()
            .ToList();

    [Fact]
    public async Task Render_UnknownTemplate_ReturnsNotFound()
    {
        var fixture = new TestDb();
        await using var db = fixture.New();

        var result = await Handler(db)
            .Handle(new RenderBumperPreviewCommand(Guid.NewGuid(), null), CancellationToken.None);

        Assert.Equal(BumperErrors.TemplateNotFound, result.Error);
    }

    [Fact]
    public async Task Render_DrawsEveryVariant_WithChannelSamplesAndAlignedDuration()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var template = NewTemplate();
        var second = template.AddVariant("Второй подблок");
        second.SetLines([
            BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "{channel}"),
        ]);

        // Шоу канала: слот сетки ссылается на группу, в группе — шоу.
        var show = Show.Create("Наше шоу", ShowKind.Series);
        var group = Group.Create("Кино");
        group.AddElement(GroupElementKind.Show, show.Id);
        var grid = ScheduleTemplate.Create(channel.Id, "Сетка");
        var slot = grid.Background!.AddSlot("Круглосуточно", new TimeOnly(6, 0), 24 * 60);
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                SlotKind.Content,
                group.Id,
                null,
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext
            )
        );
        channel.SetTemplate(grid.Id);

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.BumperTemplates.Add(template);
            seed.Shows.Add(show);
            seed.Groups.Add(group);
            seed.ScheduleTemplates.Add(grid);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var renderer = Substitute.For<IBumperRenderer>();
        await using var db = fixture.New();
        var result = await Handler(db, renderer)
            .Handle(
                new RenderBumperPreviewCommand(template.Id, channel.Id),
                CancellationToken.None
            );

        Assert.True(result.IsSuccess);
        // Оба подблока отрисованы, каждый — в свой ассет-превью.
        await renderer
            .Received(1)
            .RenderAsync(
                BumperPreview.AssetId(template.Variants[0].Id),
                Arg.Any<BumperRenderSpec>(),
                Arg.Any<CancellationToken>()
            );
        await renderer
            .Received(1)
            .RenderAsync(
                BumperPreview.AssetId(second.Id),
                Arg.Any<BumperRenderSpec>(),
                Arg.Any<CancellationToken>()
            );

        var specs = Specs(renderer);
        // Своего звука у блока нет: 8 секунд по умолчанию, выровненные вверх до сегмента в 5 сек.
        Assert.All(specs, s => Assert.Equal(10, s.DurationSeconds));

        var nowNext = specs.First(s => s.Lines.Count == 4);
        Assert.Equal("Наше шоу", nowNext.Lines[1].Text);

        var channelLine = specs.First(s => s.Lines.Count == 1);
        Assert.Equal("Первый", channelLine.Lines[0].Text);
    }

    [Fact]
    public async Task Render_WithBackgroundImage_ResolvesPathFromRegistry()
    {
        var fixture = new TestDb();
        var template = NewTemplate();
        var image = Image.Create(ImageCategory.BumperBackground, ".png", "bg.png");
        template.SetBackgroundImage(image.Id);

        await using (var seed = fixture.New())
        {
            seed.BumperTemplates.Add(template);
            seed.Images.Add(image);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var renderer = Substitute.For<IBumperRenderer>();
        var imageStore = Substitute.For<IImageStore>();
        imageStore.ResolvePath(image.Id, ".png").Returns("/data/images/bg.png");

        await using var db = fixture.New();
        var result = await Handler(db, renderer, imageStore: imageStore)
            .Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);

        Assert.True(result.IsSuccess);
        var spec = Specs(renderer)[0];
        Assert.Equal("/data/images/bg.png", spec.BackgroundFile);
        // Канала нет — образцы заглушечные, но кадр всё равно собирается.
        Assert.Equal("Первое шоу", spec.Lines[1].Text);
    }

    [Fact]
    public async Task Render_DropsLinesThatResolveToNothing()
    {
        var fixture = new TestDb();
        var template = BumperTemplate.Create("Блок", "Текст 1");
        template
            .Variants[0]
            .SetLines([
                BumperLine.Create(0, BumperLineStyle.Label, BumperLineColor.Accent, "{next.genre}"),
                BumperLine.Create(1, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
            ]);

        await using (var seed = fixture.New())
        {
            seed.BumperTemplates.Add(template);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var renderer = Substitute.For<IBumperRenderer>();
        await using var db = fixture.New();
        await Handler(db, renderer)
            .Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);

        // Жанра у образца нет — строка схлопнулась, а не оставила дыру в кадре.
        var spec = Specs(renderer)[0];
        Assert.Single(spec.Lines);
    }
}
