using TeleWave.Domain.Broadcast.Scheduling;
using TeleWave.Domain.Programming;
using TeleWave.Domain.Programming.Planning;
using Xunit;
using GridPlanner = TeleWave.Domain.Programming.Planning.SchedulePlanner;

namespace TeleWave.Domain.Tests.Programming;

public class JunctionFillerTests
{
    private static readonly DateTimeOffset T0 = new(2026, 4, 6, 18, 0, 0, TimeSpan.Zero);

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

    private static PlanningUnit Unit(double minutes, Guid? showId = null, int index = 0) =>
        new(Guid.NewGuid(), TimeSpan.FromMinutes(minutes), showId ?? Guid.NewGuid(), index);

    private static PlanningElement Series(int episodes, int minutes, out Guid showId)
    {
        showId = Guid.NewGuid();
        var id = showId;
        var units = Enumerable.Range(0, episodes).Select(i => Unit(minutes, id, i)).ToList();
        return new PlanningElement(GroupElementKind.Show, Guid.NewGuid(), 1, 0, units);
    }

    private static PlanningJunctionElement Ads(
        int count,
        double minutesEach = 0.5,
        bool required = false,
        int minMinutesBetween = 0,
        bool onlyOnElementChange = false
    ) =>
        new(
            JunctionElementKind.Ad,
            Enumerable.Range(0, 8).Select(_ => Unit(minutesEach)).ToList(),
            JunctionAmountMode.Count,
            count,
            required,
            onlyOnElementChange,
            minMinutesBetween
        );

    private static PlanningSlot Slot(
        PlanningElement element,
        int blockValue,
        PlanningJunction? between = null,
        PlanningJunction? after = null
    ) =>
        new(
            Guid.NewGuid(),
            T0,
            240,
            SlotKind.Content,
            IsAnchor: false,
            MaxDriftMinutes: 30,
            SnapToMinutes: null,
            SlotBlockMode.Count,
            blockValue,
            OverflowPolicy.ContinueNext,
            new PlanningStrategy(SlotStrategyKind.Sequential),
            [element],
            Cursor: null,
            RepeatUnits: null,
            JunctionBetween: between,
            JunctionAfter: after
        );

    private static PlanningResult Run(PlanningSlot slot) =>
        GridPlanner.Plan(
            new PlanningInput(
                Guid.NewGuid(),
                T0,
                T0.AddHours(5),
                [slot],
                [Unit(1)],
                SegmentSeconds: 2
            ),
            new FirstAlways()
        );

    [Fact]
    public void BetweenJunction_PlacesAdsBetweenUnitsButNotBeforeFirst()
    {
        var element = Series(3, 20, out _);
        var result = Run(Slot(element, 3, between: new PlanningJunction(Guid.NewGuid(), [Ads(2)])));

        var ordered = result.Items.OrderBy(i => i.StartsAtUtc).ToList();
        Assert.Equal(PlannedItemKind.Program, ordered[0].Kind);
        // Между тремя сериями два стыка по две врезки.
        Assert.Equal(4, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
    }

    [Fact]
    public void AfterJunction_PlacesAdsAtBlockEnd()
    {
        var element = Series(2, 20, out _);
        var result = Run(Slot(element, 2, after: new PlanningJunction(Guid.NewGuid(), [Ads(3)])));

        var last = result
            .Items.Where(i => i.Kind != PlannedItemKind.Fallback)
            .OrderBy(i => i.StartsAtUtc)
            .Last();
        Assert.Equal(PlannedItemKind.Ad, last.Kind);
        Assert.Equal(3, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
    }

    [Fact]
    public void DurationMode_IncludesLastUnitWholly()
    {
        var element = Series(2, 20, out _);
        var ads = new PlanningJunctionElement(
            JunctionElementKind.Ad,
            Enumerable.Range(0, 6).Select(_ => Unit(0.5)).ToList(),
            JunctionAmountMode.Duration,
            1,
            IsRequired: false
        );

        var result = Run(Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [ads])));

        // Бюджет 1 минута, ролики по 30 секунд: две штуки добирают ровно минуту, третья не нужна.
        Assert.Equal(2, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
    }

    [Fact]
    public void MinInterval_SkipsSecondJunction()
    {
        var element = Series(3, 20, out _);
        var result = Run(
            Slot(
                element,
                3,
                between: new PlanningJunction(Guid.NewGuid(), [Ads(1, minMinutesBetween: 60)])
            )
        );

        // Серии по 20 минут: второй стык приходит через 20 минут после первого — раньше разрешённого.
        Assert.Equal(1, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
    }

    [Fact]
    public void OnlyOnElementChange_SkipsWithinSameShow()
    {
        var element = Series(3, 20, out _);
        var result = Run(
            Slot(
                element,
                3,
                between: new PlanningJunction(Guid.NewGuid(), [Ads(1, onlyOnElementChange: true)])
            )
        );

        // Внутри одного сериала смены нет — врезка не ставится.
        Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Ad);
    }

    [Fact]
    public void Bumper_ReservesDurationWithEmptyAssetAndShowPair()
    {
        var element = Series(2, 20, out var showId);
        var bumper = new PlanningJunctionElement(
            JunctionElementKind.Bumper,
            [],
            JunctionAmountMode.Count,
            1,
            IsRequired: true,
            BumperTemplateId: Guid.NewGuid(),
            BumperDuration: TimeSpan.FromSeconds(10)
        );

        var result = Run(Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [bumper])));

        var placed = Assert.Single(result.Items, i => i.Kind == PlannedItemKind.Bumper);
        // Ассет подставит рендер после сборки ленты — до неё пара соседей неизвестна.
        Assert.Equal(Guid.Empty, placed.MediaAssetId);
        Assert.Equal(showId, placed.ToShowId);
        Assert.Equal(TimeSpan.FromSeconds(10), placed.EndsAtUtc - placed.StartsAtUtc);
    }

    [Fact]
    public void RequiredElement_GoesBeforeOptional()
    {
        var element = Series(2, 20, out _);
        var optional = Ads(1, minutesEach: 1);
        var required = new PlanningJunctionElement(
            JunctionElementKind.Promo,
            [Unit(0.5)],
            JunctionAmountMode.Count,
            1,
            IsRequired: true
        );

        var result = Run(
            Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [optional, required]))
        );

        var junctionItems = result
            .Items.Where(i => i.Kind is PlannedItemKind.Ad or PlannedItemKind.Promo)
            .OrderBy(i => i.StartsAtUtc)
            .ToList();
        Assert.Equal(PlannedItemKind.Promo, junctionItems[0].Kind);
    }

    [Fact]
    public void JunctionDoesNotCrossAnchor()
    {
        var element = Series(2, 20, out _);
        var slot = Slot(element, 2, after: new PlanningJunction(Guid.NewGuid(), [Ads(4, 5)])) with
        {
            TargetDurationMinutes = 40,
        };
        var anchor = new PlanningSlot(
            Guid.NewGuid(),
            T0.AddMinutes(45),
            60,
            SlotKind.Content,
            IsAnchor: true,
            MaxDriftMinutes: 5,
            SnapToMinutes: null,
            SlotBlockMode.Count,
            1,
            OverflowPolicy.ContinueNext,
            new PlanningStrategy(SlotStrategyKind.Sequential),
            [Series(1, 30, out _)],
            Cursor: null
        );

        var result = GridPlanner.Plan(
            new PlanningInput(
                Guid.NewGuid(),
                T0,
                T0.AddHours(3),
                [slot, anchor],
                [Unit(1)],
                SegmentSeconds: 2
            ),
            new FirstAlways()
        );

        // До якоря остаётся пять минут: влезает одна пятиминутная врезка, остальные не начинают.
        Assert.All(
            result.Items.Where(i => i.Kind == PlannedItemKind.Ad),
            item => Assert.True(item.EndsAtUtc <= anchor.TargetStartUtc)
        );
    }
}
