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;

/// <summary>
/// Команды блока заставки. Блок общий для всех каналов, поэтому удаление используемого запрещено:
/// иначе оно молча выключило бы заставки в чужих каналах.
/// </summary>
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<IBumperTemplateStorage>();

        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<CancellationToken>());
        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<IBumperTemplateStorage>();

        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<IBumperTemplateStorage>();
        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(string.Empty).Code, used.Error.Code);
        }

        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<CancellationToken>());
        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<IBumperTemplateStorage>()
        ).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);
    }
}
