using TeleWave.Domain.Broadcast.Scheduling;
using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;
using TeleWave.Domain.Programming.Planning;
using Xunit;

namespace TeleWave.Domain.Tests.Programming;

/// <summary>
/// Жёсткие фильтры кандидатов (см. 3.8): детское время и потолок повторов. Оба отсекают до жребия,
/// поэтому проверяются на выборе элемента, а не на готовой ленте.
/// </summary>
public class CandidateFilterTests
{
    private static readonly DateTimeOffset T0 = new(2026, 1, 5, 12, 0, 0, TimeSpan.Zero);

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

    private static PlanningElement Element(
        ShowAudience? audience = null,
        int position = 0,
        DateTimeOffset? lastPlayed = null,
        IReadOnlyList<DateTimeOffset>? recentPlays = null
    )
    {
        var showId = Guid.NewGuid();
        return new PlanningElement(
            GroupElementKind.Show,
            Guid.NewGuid(),
            Weight: 1,
            position,
            [new PlanningUnit(Guid.NewGuid(), TimeSpan.FromMinutes(30), showId, 0)],
            lastPlayed,
            audience,
            recentPlays
        );
    }

    private static PlanningSlot Slot(
        IReadOnlyList<PlanningElement> elements,
        ShowAudience? maxAudience = null,
        RepeatLimit? repeatLimit = null,
        SlotStrategyKind strategy = SlotStrategyKind.RandomWithCooldown
    ) =>
        new(
            Guid.NewGuid(),
            T0,
            TargetDurationMinutes: 60,
            SlotKind.Content,
            IsAnchor: false,
            MaxDriftMinutes: 30,
            SnapToMinutes: null,
            SlotBlockMode.FillSlot,
            BlockValue: 1,
            OverflowPolicy.ContinueNext,
            new PlanningStrategy(strategy),
            elements,
            Cursor: null,
            MaxAudience: maxAudience,
            RepeatLimit: repeatLimit
        );

    [Fact]
    public void Audience_FiltersOutStricterContent()
    {
        var forEveryone = Element(ShowAudience.G);
        var adult = Element(ShowAudience.Nc17, position: 1);

        var pick = ElementSelector.Select(
            Slot([adult, forEveryone], maxAudience: ShowAudience.Pg),
            T0,
            new FirstAlways()
        );

        Assert.NotNull(pick);
        Assert.Equal(forEveryone.ElementId, pick.Element.ElementId);
    }

    [Fact]
    public void Audience_WithoutLimit_KeepsEverything()
    {
        var adult = Element(ShowAudience.Nc17);

        var pick = ElementSelector.Select(Slot([adult]), T0, new FirstAlways());

        Assert.NotNull(pick);
        Assert.Equal(adult.ElementId, pick.Element.ElementId);
    }

    [Fact]
    public void Audience_UnknownRating_IsNotDropped()
    {
        // Непроставленный рейтинг — не повод выбрасывать: иначе контент исчезал бы из эфира молча.
        var unknown = Element(audience: null);

        var pick = ElementSelector.Select(
            Slot([unknown], maxAudience: ShowAudience.G),
            T0,
            new FirstAlways()
        );

        Assert.NotNull(pick);
        Assert.Equal(unknown.ElementId, pick.Element.ElementId);
    }

    [Fact]
    public void Audience_AllFilteredOut_ReturnsNothing()
    {
        var pick = ElementSelector.Select(
            Slot([Element(ShowAudience.Nc17)], maxAudience: ShowAudience.G),
            T0,
            new FirstAlways()
        );

        Assert.Null(pick);
    }

    [Fact]
    public void Audience_AppliesToSequentialStrategyToo()
    {
        var adult = Element(ShowAudience.Nc17, position: 0);
        var teen = Element(ShowAudience.Pg13, position: 1);

        var pick = ElementSelector.Select(
            Slot(
                [adult, teen],
                maxAudience: ShowAudience.Pg13,
                strategy: SlotStrategyKind.Sequential
            ),
            T0,
            new FirstAlways()
        );

        Assert.NotNull(pick);
        Assert.Equal(teen.ElementId, pick.Element.ElementId);
    }

    [Fact]
    public void RepeatLimit_DropsElementsOverTheCap()
    {
        var overCap = Element(recentPlays: [T0.AddDays(-1), T0.AddDays(-2)]);
        var fresh = Element(position: 1, recentPlays: [T0.AddDays(-1)]);

        var pick = ElementSelector.Select(
            Slot([overCap, fresh], repeatLimit: new RepeatLimit(WindowDays: 7, Max: 2)),
            T0,
            new FirstAlways()
        );

        Assert.NotNull(pick);
        Assert.Equal(fresh.ElementId, pick.Element.ElementId);
    }

    [Fact]
    public void RepeatLimit_IgnoresPlaysOutsideTheWindow()
    {
        // Показы старше окна не считаются — иначе правило «не чаще двух раз в неделю» запирало бы
        // элемент навсегда.
        var old = Element(recentPlays: [T0.AddDays(-30), T0.AddDays(-31)]);

        var pick = ElementSelector.Select(
            Slot([old], repeatLimit: new RepeatLimit(WindowDays: 7, Max: 2)),
            T0,
            new FirstAlways()
        );

        Assert.NotNull(pick);
        Assert.Equal(old.ElementId, pick.Element.ElementId);
    }

    [Fact]
    public void RepeatLimit_WhenEveryoneIsOverTheCap_StillPicksSomething()
    {
        // Пустой эфир хуже раннего повтора — как и при исчерпанном остывании.
        var a = Element(recentPlays: [T0.AddDays(-1), T0.AddDays(-2)]);
        var b = Element(position: 1, recentPlays: [T0.AddDays(-1), T0.AddDays(-2)]);

        var pick = ElementSelector.Select(
            Slot([a, b], repeatLimit: new RepeatLimit(WindowDays: 7, Max: 1)),
            T0,
            new FirstAlways()
        );

        Assert.NotNull(pick);
    }

    [Fact]
    public void RepeatLimit_AppliesBeforeCooldown()
    {
        // Порядок проверяется через исчерпание: потолок повторов выбрасывает «давний» элемент ещё
        // до остывания, поэтому в вырожденном случае остаётся свежий. В обратном порядке остывание
        // оставило бы давний, и выбор был бы другим.
        var overCap = Element(
            lastPlayed: T0.AddDays(-10),
            recentPlays: [T0.AddDays(-10), T0.AddDays(-9), T0.AddDays(-8)]
        );
        var recent = Element(
            position: 1,
            lastPlayed: T0.AddHours(-1),
            recentPlays: [T0.AddHours(-1)]
        );

        var slot = Slot(
            [overCap, recent],
            repeatLimit: new RepeatLimit(WindowDays: 30, Max: 2)
        ) with
        {
            Strategy = new PlanningStrategy(SlotStrategyKind.RandomWithCooldown, CooldownDays: 2),
        };

        var pick = ElementSelector.Select(slot, T0, new FirstAlways());

        Assert.NotNull(pick);
        Assert.Equal(recent.ElementId, pick.Element.ElementId);
    }
}
