using TeleWave.Application.Broadcast;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.Validate;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

/// <summary>
/// Проверки сетки до генерации: пустые группы, пересечения слотов, недостижимое остывание,
/// конфликт с детским временем и непокрытые интервалы суток.
/// </summary>
public class ValidateTemplateTests
{
    private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);

    /// <summary>Слот на сутки целиком — им закрываем сетку, когда дыры не тема теста.</summary>
    private static Slot FullDaySlot(GridLayer layer, Guid? groupId, TimeOnly dayStart)
    {
        var slot = layer.AddSlot("Круглосуточно", dayStart, 24 * 60);
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                SlotKind.Content,
                groupId,
                new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext
            )
        );
        return slot;
    }

    private static void Fill(
        Slot slot,
        Guid? groupId,
        SlotStrategy? strategy = null,
        SlotKind kind = SlotKind.Content
    ) =>
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                kind,
                groupId,
                (strategy ?? new SlotStrategy(SlotStrategyType.Sequential)).ToJson(),
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext
            )
        );

    /// <summary>Наполняет группу позициями: проверке важно их число, а не что за ними стоит.</summary>
    private static void FillGroup(Group group, int count)
    {
        for (var i = 0; i < count; i++)
            group.AddElement(GroupElementKind.Show, Guid.NewGuid());
    }

    private static async Task<IReadOnlyList<TemplateIssueDto>> ValidateAsync(
        TestDb fixture,
        Guid channelId
    )
    {
        await using var db = fixture.New();
        var result = await new ValidateTemplateQueryHandler(db, GroupServices.Dynamic(db)).Handle(
            new ValidateTemplateQuery(channelId),
            CancellationToken.None
        );
        Assert.True(result.IsSuccess);
        return result.Value;
    }

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

        var result = await new ValidateTemplateQueryHandler(db, GroupServices.Dynamic(db)).Handle(
            new ValidateTemplateQuery(Guid.NewGuid()),
            CancellationToken.None
        );

        Assert.Equal(ChannelErrors.NotFound, result.Error);
    }

    [Fact]
    public async Task Validate_ChannelWithoutGrid_ReturnsTemplateNotFound()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Без сетки", "nogrid", T0);
        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var result = await new ValidateTemplateQueryHandler(db, GroupServices.Dynamic(db)).Handle(
            new ValidateTemplateQuery(channel.Id),
            CancellationToken.None
        );

        Assert.Equal(ChannelErrors.TemplateNotFound, result.Error);
    }

    [Fact]
    public async Task Validate_EmptyGrid_ReportsGapForEveryDay()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.ScheduleTemplates.Add(template);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var issues = await ValidateAsync(fixture, channel.Id);

        // Семь суток без единого слота — семь дыр «весь день».
        var gaps = issues.Where(i => i.Kind == TemplateIssueKind.GridGap).ToList();
        Assert.Equal(7, gaps.Count);
        Assert.All(gaps, g => Assert.Contains("весь день", g.Details));
        Assert.All(gaps, g => Assert.Equal(TemplateIssueSeverity.Error, g.Severity));
    }

    [Fact]
    public async Task Validate_SlotWithoutGroup_ReportsGroupMissing()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        FullDaySlot(template.Background!, null, channel.DayStartTime);

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.ScheduleTemplates.Add(template);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var issues = await ValidateAsync(fixture, channel.Id);

        Assert.Contains(issues, i => i.Kind == TemplateIssueKind.GroupMissing);
        // Сутки закрыты слотом — дыр быть не должно.
        Assert.DoesNotContain(issues, i => i.Kind == TemplateIssueKind.GridGap);
    }

    [Fact]
    public async Task Validate_EmptyGroup_ReportsGroupEmpty()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var group = Group.Create("Пустая");
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        FullDaySlot(template.Background!, group.Id, channel.DayStartTime);

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.Groups.Add(group);
            seed.ScheduleTemplates.Add(template);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var issues = await ValidateAsync(fixture, channel.Id);

        var issue = Assert.Single(issues, i => i.Kind == TemplateIssueKind.GroupEmpty);
        Assert.Contains("Пустая", issue.Details);
    }

    [Fact]
    public async Task Validate_SmallGroupWithCooldown_ReportsBothWarnings()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var group = Group.Create("Маленькая");
        // Позиции настоящие: проверка считает состав, а не кэш статистики, — у динамической группы
        // кэш отстаёт от библиотеки, и полагаться на него нельзя.
        FillGroup(group, 3);

        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        var slot = FullDaySlot(template.Background!, group.Id, channel.DayStartTime);
        // Ежедневный слот — 7 выходов в неделю на 3 позиции, да ещё с двухнедельным остыванием.
        Fill(
            slot,
            group.Id,
            new SlotStrategy(SlotStrategyType.RandomWithCooldown, CooldownDays: 14)
        );

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.Groups.Add(group);
            seed.ScheduleTemplates.Add(template);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var issues = await ValidateAsync(fixture, channel.Id);

        Assert.Contains(issues, i => i.Kind == TemplateIssueKind.GroupTooSmall);
        Assert.Contains(issues, i => i.Kind == TemplateIssueKind.CooldownUnreachable);
        Assert.All(
            issues.Where(i => i.Kind != TemplateIssueKind.GridGap),
            i => Assert.Equal(TemplateIssueSeverity.Warning, i.Severity)
        );
    }

    [Fact]
    public async Task Validate_OverlappingSlotsInOneLayer_AreReported()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var group = Group.Create("Кино");
        FillGroup(group, 50);

        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        FullDaySlot(template.Background!, group.Id, channel.DayStartTime);
        var layer = template.AddLayer("Прайм", 10);
        var first = layer.AddSlot("Кино", new TimeOnly(20, 0), 120);
        Fill(first, group.Id);
        var second = layer.AddSlot("Сериал", new TimeOnly(21, 0), 60);
        Fill(second, group.Id);

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.Groups.Add(group);
            seed.ScheduleTemplates.Add(template);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var issues = await ValidateAsync(fixture, channel.Id);

        // Внутри слоя приоритетов нет: второй слот молча пропал бы при генерации.
        var overlap = Assert.Single(issues, i => i.Kind == TemplateIssueKind.SlotOverlap);
        Assert.Equal(second.Id, overlap.SlotId);
        Assert.Equal(layer.Id, overlap.LayerId);
    }

    [Fact]
    public async Task Validate_AdultContentInKidsWindow_ReportsAudienceConflict()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);

        var mild = Show.Create("Мультик", ShowKind.Single);
        mild.SetAudience(ShowAudience.G);
        var harsh = Show.Create("Ужастик", ShowKind.Single);
        harsh.SetAudience(ShowAudience.R);
        // Строжайший рейтинг группы приходит через коллекцию: франшиза идёт целиком.
        var collection = Collection.Create("Франшиза");
        collection.AddShow(harsh.Id);

        var group = Group.Create("Вечернее");
        group.AddElement(GroupElementKind.Show, mild.Id);
        group.AddElement(GroupElementKind.Collection, collection.Id);
        FillGroup(group, 2);

        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        var slot = FullDaySlot(template.Background!, group.Id, channel.DayStartTime);
        slot.UpdateTiming(
            null,
            new TimeOnly(10, 0),
            24 * 60,
            Daypart.Day,
            isAnchor: false,
            5,
            null
        );
        template.SetRules(
            new PlanningRules(
                MaxAudienceByTime:
                [
                    new AudienceWindow(new TimeOnly(6, 0), new TimeOnly(21, 0), ShowAudience.Pg),
                ]
            ).ToJson()
        );

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.Groups.Add(group);
            seed.Collections.Add(collection);
            seed.Shows.AddRange(mild, harsh);
            seed.ScheduleTemplates.Add(template);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var issues = await ValidateAsync(fixture, channel.Id);

        var conflict = Assert.Single(issues, i => i.Kind == TemplateIssueKind.AudienceConflict);
        Assert.Contains("Вечернее", conflict.Details);
    }
}
