using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.Generate;
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.Programming;

/// <summary>
/// Витрина автосборки: каталог профилей, предпросмотр плана и его перевод в DTO. Предпросмотр —
/// то, по чему админ принимает решение, поэтому проверяется отдельно от самой раскладки.
/// </summary>
public class GridPlanPreviewTests
{
    private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);

    private static PlannedSlot Slot(
        string title,
        SlotKind kind,
        SlotBlockMode blockMode,
        int blockValue,
        int? weekday = null
    ) =>
        new(
            GridPlanLayer.Main,
            weekday,
            new TimeOnly(20, 0),
            120,
            title,
            Daypart.Prime,
            kind,
            kind == SlotKind.Content ? Guid.NewGuid() : null,
            kind == SlotKind.Content ? title : null,
            blockMode,
            blockValue,
            null,
            null,
            IsAnchor: false
        );

    [Fact]
    public async Task Profiles_AreListed_WithReferenceAndDescription()
    {
        var result = await new ListGridProfilesQueryHandler().Handle(
            new ListGridProfilesQuery(),
            CancellationToken.None
        );

        Assert.True(result.IsSuccess);
        // Каждый профиль должен объяснять себя: имя, на что похоже и для какой библиотеки годится.
        Assert.Equal(GridProfiles.All.Count, result.Value.Count);
        Assert.All(
            result.Value,
            profile =>
            {
                Assert.False(string.IsNullOrWhiteSpace(profile.Name));
                Assert.False(string.IsNullOrWhiteSpace(profile.Reference));
                Assert.False(string.IsNullOrWhiteSpace(profile.Description));
            }
        );
        Assert.Contains(result.Value, p => p.Kind == GridProfileKind.Animation);
    }

    [Fact]
    public void Mapper_DescribesEveryBlockKind()
    {
        var plan = new GridPlan(
            GridProfiles.Get(GridProfileKind.Mixed),
            GridGenerationMode.Rebuild,
            [
                Slot("Сериалы", SlotKind.Content, SlotBlockMode.Count, 3),
                Slot("Кино", SlotKind.Content, SlotBlockMode.FillSlot, 1),
                Slot("Полоса", SlotKind.Content, SlotBlockMode.Duration, 90, weekday: 6),
                Slot("Повтор", SlotKind.Repeat, SlotBlockMode.FillSlot, 1),
                Slot("Конец вещания", SlotKind.SignOff, SlotBlockMode.FillSlot, 1),
            ],
            [new PlanNote(false, "замечание")],
            SlotsToRemove: 4,
            FallbackGroupId: Guid.NewGuid(),
            FallbackGroupName: "Фон",
            FreeMinutes: 10080,
            CoveredMinutes: 5040
        );

        var dto = plan.ToDto();

        Assert.Equal(GridProfileKind.Mixed, dto.Profile);
        Assert.Equal(plan.Profile.Name, dto.ProfileName);
        Assert.Equal(4, dto.SlotsToRemove);
        Assert.Equal("Фон", dto.FallbackGroupName);
        Assert.Equal(["замечание"], dto.Notes.Select(n => n.Text));

        // Описание блока — то, что читает админ вместо режима и числа.
        Assert.Equal(
            ["3 подряд", "до конца слота", "90 мин", "повтор", "конец вещания"],
            dto.Slots.Select(s => s.Block)
        );
        // Время приезжает строкой «ЧЧ:ММ»: в предпросмотре оно только показывается.
        Assert.All(dto.Slots, slot => Assert.Equal("20:00", slot.Start));
        Assert.Equal(6, dto.Slots[2].Weekday);
        Assert.Null(dto.Slots[0].Weekday);
        // У неконтентных слотов группы нет — показывать в колонке нечего.
        Assert.Null(dto.Slots[3].GroupName);
        Assert.Equal("Сериалы", dto.Slots[0].GroupName);
    }

    [Fact]
    public async Task Preview_ReturnsPlanForProfile()
    {
        var fixture = new TestDb();
        var channelId = await SeedAsync(fixture);

        await using var db = fixture.New();
        var result = await new PreviewGeneratedGridQueryHandler(
            new GridPlanner(
                db,
                new GroupCatalog(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
            )
        ).Handle(
            new PreviewGeneratedGridQuery(
                channelId,
                GridProfileKind.Kids,
                GridGenerationMode.Rebuild
            ),
            CancellationToken.None
        );

        Assert.True(result.IsSuccess);
        Assert.Equal(GridProfileKind.Kids, result.Value.Profile);
        Assert.NotEmpty(result.Value.Slots);
        // Пересборка сносит то, что было, и это видно до нажатия.
        Assert.True(result.Value.SlotsToRemove >= 0);
        Assert.Equal(7 * GridCoverage.MinutesInDay, result.Value.FreeMinutes);
        // У детского профиля ночь — конец вещания, и в предпросмотре он подписан именно так.
        Assert.Contains(result.Value.Slots, s => s.Block == "конец вещания");
    }

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

        var result = await new PreviewGeneratedGridQueryHandler(
            new GridPlanner(
                db,
                new GroupCatalog(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
            )
        ).Handle(
            new PreviewGeneratedGridQuery(
                Guid.NewGuid(),
                GridProfileKind.Mixed,
                GridGenerationMode.FillGaps
            ),
            CancellationToken.None
        );

        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Validator_RejectsUnknownProfileAndMode()
    {
        var validator = new GenerateGridCommandValidator();

        Assert.True(
            validator
                .Validate(
                    new GenerateGridCommand(
                        Guid.NewGuid(),
                        GridProfileKind.Sitcom,
                        GridGenerationMode.Rebuild
                    )
                )
                .IsValid
        );
        // Значения enum приходят с клиента строкой — за пределами каталога они бессмысленны.
        Assert.False(
            validator
                .Validate(
                    new GenerateGridCommand(
                        Guid.NewGuid(),
                        (GridProfileKind)42,
                        GridGenerationMode.FillGaps
                    )
                )
                .IsValid
        );
    }

    /// <summary>Канал с одной группой мягкого рейтинга — чтобы детскому профилю было чем закрыться.</summary>
    private static async Task<Guid> SeedAsync(TestDb fixture)
    {
        var show = Show.Create("Мультсериал", ShowKind.Series);
        show.SetAudience(ShowAudience.G);
        var assets = new List<MediaAsset>();

        for (var i = 0; i < 20; i++)
        {
            var asset = MediaAsset.Register($"e{i}.mkv", ".mkv", MediaSource.Upload);
            asset.MarkProcessing();
            asset.MarkReady(
                new MediaReadyInfo(
                    TimeSpan.FromMinutes(22),
                    6,
                    220,
                    1920,
                    1080,
                    "h264",
                    "aac",
                    $"assets/e{i}"
                )
            );
            assets.Add(asset);
            show.AddEpisode(asset.Id);
        }

        var group = Group.Create("Мультфильмы");
        group.AddElement(GroupElementKind.Show, show.Id);

        var channel = Channel.Create("Детский", "kids", T0);
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        channel.SetTemplate(template.Id);

        await using var seed = fixture.New();
        seed.MediaAssets.AddRange(assets);
        seed.Shows.Add(show);
        seed.Groups.Add(group);
        seed.Channels.Add(channel);
        seed.ScheduleTemplates.Add(template);
        await seed.SaveChangesAsync(CancellationToken.None);

        return channel.Id;
    }
}
