using System.Globalization;
using System.Text;
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;

/// <summary>
/// Золотые тесты планировщика: для набора эталонных конфигураций фиксируется посуточная лента
/// целиком. Любая правка стратегий или подгонки сразу показывает построчный диф — что именно
/// поехало, — вместо «упало N тестов» через неделю после того, как это ушло в эфир.
///
/// Эталон обновляется осознанно: переписыванием ожидаемой строки в тесте, а не автоматически при
/// расхождении. Автообновление превратило бы их в тесты, которые всегда зелёные.
/// </summary>
public class GoldenScheduleTests
{
    private static readonly DateTimeOffset Day = new(2026, 3, 2, 6, 0, 0, TimeSpan.Zero);

    /// <summary>Псевдослучайность с фиксированным зерном: жребий обязан быть воспроизводимым.</summary>
    private sealed class SeededRandom(int seed) : IRandomSource
    {
        private int _state = seed;

        public int Next(int maxExclusive)
        {
            if (maxExclusive <= 0)
                return 0;
            _state = (int)((_state * 1103515245L + 12345) & 0x7FFFFFFF);
            return _state % maxExclusive;
        }
    }

    /// <summary>Лента в компактном виде: время начала, тип, шоу и номер единицы — построчно.</summary>
    private static List<string> Render(
        PlanningResult result,
        IReadOnlyDictionary<Guid, string> names
    ) =>
        result
            .Items.OrderBy(i => i.StartsAtUtc)
            .Select(item =>
            {
                var label =
                    item.ShowId is { } showId && names.TryGetValue(showId, out var name)
                        ? $"{name}#{item.UnitIndex}"
                        : item.Kind.ToString();
                return string.Create(
                    CultureInfo.InvariantCulture,
                    $"{item.StartsAtUtc:HH:mm} {label}"
                );
            })
            .ToList();

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

    private static PlanningSlot ContentSlot(
        DateTimeOffset start,
        int durationMinutes,
        IReadOnlyList<PlanningElement> elements,
        SlotBlockMode blockMode,
        int blockValue,
        bool isAnchor = false,
        int? snap = null
    ) =>
        new(
            Guid.NewGuid(),
            start,
            durationMinutes,
            SlotKind.Content,
            isAnchor,
            MaxDriftMinutes: 15,
            snap,
            blockMode,
            blockValue,
            OverflowPolicy.ContinueNext,
            new PlanningStrategy(SlotStrategyKind.Sequential),
            elements,
            Cursor: null
        );

    private static PlanningUnit Filler() =>
        new(Guid.NewGuid(), TimeSpan.FromMinutes(1), Guid.Empty, 0);

    [Fact]
    public void Golden_MorningCartoonsThenPrimeMovie()
    {
        var (cartoons, cartoonShow) = Series(6, 20);
        var (movie, movieShow) = Series(1, 95);

        var slots = new[]
        {
            // Утренний блок: три мультфильма подряд.
            ContentSlot(Day.AddHours(1), 60, [cartoons], SlotBlockMode.Count, 3),
            // Прайм с якорем: старт жёсткий, предыдущий блок через него не перелезает.
            ContentSlot(Day.AddHours(14), 120, [movie], SlotBlockMode.Count, 1, isAnchor: true),
        };

        var input = new PlanningInput(
            Guid.NewGuid(),
            Day,
            Day.AddHours(17),
            slots,
            [Filler()],
            SegmentSeconds: 2
        );

        var result = GridPlanner.Plan(input, new SeededRandom(42));
        var names = new Dictionary<Guid, string>
        {
            [cartoonShow] = "Мультфильмы",
            [movieShow] = "Кино",
        };

        var tape = Render(result, names)
            .Where(line => !line.Contains("Fallback", StringComparison.Ordinal))
            .ToList();

        Assert.Equal(
            ["07:00 Мультфильмы#0", "07:20 Мультфильмы#1", "07:40 Мультфильмы#2", "20:00 Кино#0"],
            tape
        );
    }

    [Fact]
    public void Golden_DriftIsCarriedForward()
    {
        var (series, showId) = Series(6, 25);

        // Три слота подряд по часу, блок — две серии по 25 минут: каждый слот отдаёт 50 минут,
        // и разбег накапливается, пока его не подберёт следующий целевой старт.
        var slots = Enumerable
            .Range(0, 3)
            .Select(i => ContentSlot(Day.AddHours(i), 60, [series], SlotBlockMode.Count, 2))
            .ToArray();

        var input = new PlanningInput(
            Guid.NewGuid(),
            Day,
            Day.AddHours(4),
            slots,
            [Filler()],
            SegmentSeconds: 2
        );

        var result = GridPlanner.Plan(input, new SeededRandom(7));
        var programs = result
            .Items.Where(i => i.Kind == PlannedItemKind.Program)
            .OrderBy(i => i.StartsAtUtc)
            .Select(i => i.StartsAtUtc.ToString("HH:mm", CultureInfo.InvariantCulture))
            .ToList();

        // Слот не начинается раньше своего целевого времени: недобор закрывает фон, а не сдвиг вперёд.
        Assert.Equal(["06:00", "06:25", "07:00", "07:25", "08:00", "08:25"], programs);
        Assert.Contains(result.Items, i => i.Kind == PlannedItemKind.Fallback);
        Assert.Equal(showId, result.Items.First(i => i.Kind == PlannedItemKind.Program).ShowId);
    }

    [Fact]
    public void Golden_SameInputProducesSameTape()
    {
        var (series, _) = Series(10, 30);
        var slots = new[] { ContentSlot(Day, 180, [series], SlotBlockMode.FillSlot, 1) };
        // Один и тот же филлер на оба прогона: разные ассеты фона сделали бы сравнение бессмысленным.
        var filler = Filler();

        PlanningInput MakeInput() =>
            new(Guid.NewGuid(), Day, Day.AddHours(4), slots, [filler], SegmentSeconds: 2);

        var first = GridPlanner.Plan(MakeInput(), new SeededRandom(1));
        var second = GridPlanner.Plan(MakeInput(), new SeededRandom(1));

        // Один вход и одно зерно обязаны давать одну ленту: иначе предпросмотр разойдётся с эфиром.
        Assert.Equal(
            first.Items.Select(i => (i.StartsAtUtc, i.MediaAssetId)),
            second.Items.Select(i => (i.StartsAtUtc, i.MediaAssetId))
        );
    }
}
