using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NSubstitute;
using TeleWave.Application.Broadcast.Scheduling;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Planning;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Streaming;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Broadcast.Scheduling;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using TeleWave.Domain.Programming.Planning;
using TeleWave.Infrastructure.Persistence;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

/// <summary>
/// Разворачивание стыка генератором: то, что происходит между шаблоном стыка в базе и чистым
/// планировщиком. Проверяется сухим прогоном — он ничего не пишет, но проходит тот же сбор входа.
/// </summary>
public class GridJunctionPreviewTests
{
    private static readonly DateTimeOffset T0 = new(2026, 4, 6, 6, 0, 0, TimeSpan.Zero);

    private sealed class FirstAlways : IRandomSource
    {
        public int Next(int maxExclusive) => 0;
    }

    private static GridScheduleGenerator Generator(AppDbContext db) =>
        new(
            db,
            new GroupExpander(db, GroupServices.Dynamic(db)),
            new BumperResolver(db, Substitute.For<IBumperRenderQueue>(), new FirstAlways()),
            new PostCheckRunner(db),
            new FirstAlways(),
            Options.Create(new SchedulerOptions { HorizonDays = 1, RetentionDays = 90 }),
            Options.Create(new StreamingOptions())
        );

    private static MediaAsset ReadyAsset(string name, TimeSpan duration)
    {
        var asset = MediaAsset.Register(name, ".mkv", MediaSource.Upload);
        asset.MarkProcessing();
        asset.MarkReady(
            new MediaReadyInfo(
                duration,
                2,
                (int)(duration.TotalSeconds / 2),
                1920,
                1080,
                "h264",
                "aac",
                "assets/x"
            )
        );
        return asset;
    }

    /// <summary>Канал с одним слотом на сутки, группой из двух серий и группой роликов для врезок.</summary>
    private static async Task<(
        TestDb Fixture,
        Guid ChannelId,
        JunctionTemplate Junction
    )> SeedAsync(Daypart daypart = Daypart.Prime)
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);

        var show = Show.Create("Симпсоны", ShowKind.Series);
        var episodes = new[]
        {
            ReadyAsset("s01e01.mkv", TimeSpan.FromMinutes(20)),
            ReadyAsset("s01e02.mkv", TimeSpan.FromMinutes(20)),
        };
        foreach (var episode in episodes)
            show.AddEpisode(episode.Id);

        var spot = Show.Create("Ролик", ShowKind.Interstitial);
        var spotAsset = ReadyAsset("spot.mkv", TimeSpan.FromSeconds(30));
        spot.AddEpisode(spotAsset.Id);

        var content = Group.Create("Мультсериалы");
        content.AddElement(GroupElementKind.Show, show.Id);
        var ads = Group.Create("Реклама");
        ads.AddElement(GroupElementKind.Show, spot.Id);

        var bumper = BumperTemplate.Create("Блок", "Текст 1");
        bumper.SetAudio(".mp3", 8);
        bumper
            .Variants[0]
            .SetLines([
                BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
            ]);

        var junction = JunctionTemplate.Create("Прайм");
        junction.Update("Прайм", 600);
        junction
            .AddElement(JunctionElementKind.Ad)
            .Update(
                new JunctionElementSettings(
                    JunctionElementKind.Ad,
                    ads.Id,
                    null,
                    null,
                    JunctionAmountMode.Count,
                    1,
                    true,
                    null,
                    1,
                    null
                )
            );
        junction
            .AddElement(JunctionElementKind.Bumper)
            .Update(
                new JunctionElementSettings(
                    JunctionElementKind.Bumper,
                    null,
                    bumper.Id,
                    null,
                    JunctionAmountMode.Count,
                    1,
                    false,
                    null,
                    1,
                    // Только в прайме: дейпарт — свойство слота, и отсекается он при сборке входа.
                    new JunctionConditions(Dayparts: [Daypart.Prime]).ToJson()
                )
            );

        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        var slot = template.Background!.AddSlot("Дневной блок", new TimeOnly(6, 0), 24 * 60);
        slot.UpdateTiming(null, new TimeOnly(6, 0), 24 * 60, daypart, false, 30, null);
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                SlotKind.Content,
                content.Id,
                new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
                null,
                SlotBlockMode.Count,
                2,
                OverflowPolicy.ContinueNext,
                junction.Id
            )
        );
        channel.SetTemplate(template.Id);

        await using var seed = fixture.New();
        seed.Channels.Add(channel);
        seed.Shows.AddRange(show, spot);
        seed.MediaAssets.AddRange([.. episodes, spotAsset]);
        seed.Groups.AddRange(content, ads);
        seed.BumperTemplates.Add(bumper);
        seed.JunctionTemplates.Add(junction);
        seed.ScheduleTemplates.Add(template);
        await seed.SaveChangesAsync(CancellationToken.None);

        return (fixture, channel.Id, junction);
    }

    [Fact]
    public async Task Preview_ExpandsJunctionWithAdsAndBumperReservation()
    {
        var (fixture, channelId, _) = await SeedAsync();

        await using var db = fixture.New();
        var result = await Generator(db).PreviewAsync(channelId, T0, 1, CancellationToken.None);

        Assert.NotNull(result);
        // Слот выходит в каждые вещательные сутки горизонта — врезки повторяются вместе с ним.
        var ads = result.Items.Where(i => i.Kind == PlannedItemKind.Ad).ToList();
        var bumpers = result.Items.Where(i => i.Kind == PlannedItemKind.Bumper).ToList();
        Assert.NotEmpty(ads);
        Assert.Equal(ads.Count, bumpers.Count);

        var ad = ads[0];
        var bumper = bumpers[0];
        // Заставка резервирует длину по звуку (8 с), выровненную на сегмент.
        Assert.Equal(TimeSpan.FromSeconds(8), bumper.EndsAtUtc - bumper.StartsAtUtc);
        Assert.Equal(TimeSpan.FromSeconds(30), ad.EndsAtUtc - ad.StartsAtUtc);
        // Ассет заставки в сухом прогоне не рендерится — только место под него.
        Assert.Equal(Guid.Empty, bumper.MediaAssetId);
        Assert.NotNull(bumper.BumperTemplateId);
    }

    [Fact]
    public async Task Preview_DropsBreaksOutsideTheirDayparts()
    {
        var (fixture, channelId, _) = await SeedAsync(Daypart.Night);

        await using var db = fixture.New();
        var result = await Generator(db).PreviewAsync(channelId, T0, 1, CancellationToken.None);

        Assert.NotNull(result);
        // Заставка разрешена только в прайме, а слот ночной — в ленте её быть не должно.
        Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Bumper);
        Assert.Contains(result.Items, i => i.Kind == PlannedItemKind.Ad);
    }

    [Fact]
    public async Task Preview_SkipsBreaksWithoutSource()
    {
        var (fixture, channelId, junction) = await SeedAsync();

        await using (var db = fixture.New())
        {
            // Врезка ссылается на блок, которого нет: настроена не до конца — играть ей нечем.
            var stored = await JunctionWithElementsAsync(db, junction.Id);
            var element = stored.Elements.Single(e => e.Kind == JunctionElementKind.Bumper);
            element.Update(
                new JunctionElementSettings(
                    JunctionElementKind.Bumper,
                    null,
                    Guid.NewGuid(),
                    null,
                    JunctionAmountMode.Count,
                    1,
                    false,
                    null,
                    1,
                    null
                )
            );
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var context = fixture.New();
        var result = await Generator(context)
            .PreviewAsync(channelId, T0, 1, CancellationToken.None);

        Assert.NotNull(result);
        Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Bumper);
    }

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

        Assert.Null(
            await Generator(db).PreviewAsync(Guid.NewGuid(), T0, 1, CancellationToken.None)
        );
    }

    private static Task<JunctionTemplate> JunctionWithElementsAsync(
        AppDbContext db,
        Guid junctionId
    ) => db.JunctionTemplates.Include(j => j.Elements).SingleAsync(j => j.Id == junctionId);
}
