using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.CreateSlot;
using TeleWave.Application.Programming.Templates.DeleteSlot;
using TeleWave.Application.Programming.Templates.GetTemplate;
using TeleWave.Application.Programming.Templates.Layers;
using TeleWave.Application.Programming.Templates.UpdateSlot;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Programming;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

/// <summary>
/// Правка сетки: слои, слоты и настройки шаблона. Проверки здесь те, что валидатору не по силам —
/// им нужны соседние слоты и справочник групп, — плюс инвариант «любая правка двигает ревизию».
/// </summary>
public class TemplateEditingTests
{
    private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);

    private static SlotInput Input(
        string title = "Вечернее кино",
        TimeOnly? start = null,
        int minutes = 120,
        SlotKind kind = SlotKind.Content,
        Guid? groupId = null,
        RepeatSource? repeat = null,
        int? weekday = null
    ) =>
        new(
            title,
            weekday,
            start ?? new TimeOnly(20, 0),
            minutes,
            Daypart.Prime,
            kind,
            groupId,
            new SlotStrategy(SlotStrategyType.Sequential),
            repeat,
            SlotBlockMode.FillSlot,
            1,
            OverflowPolicy.ContinueNext,
            IsAnchor: true,
            MaxDriftMinutes: 10,
            SnapToMinutes: 15
        );

    /// <summary>Канал с сеткой: один обычный слой поверх фонового плюс группа контента.</summary>
    private sealed record Fixture(
        TestDb Db,
        Guid ChannelId,
        Guid TemplateId,
        Guid LayerId,
        Guid BackgroundLayerId,
        Guid GroupId
    );

    private static async Task<Fixture> SeedAsync()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var group = Group.Create("Кино");
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        var layer = template.AddLayer("Прайм", 10);
        channel.SetTemplate(template.Id);

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

        return new Fixture(
            fixture,
            channel.Id,
            template.Id,
            layer.Id,
            template.Background!.Id,
            group.Id
        );
    }

    private static async Task<Guid> AddSlotAsync(Fixture f, SlotInput input)
    {
        await using var db = f.Db.New();
        var created = await new CreateSlotCommandHandler(GroupServices.Slots(db)).Handle(
            new CreateSlotCommand(f.LayerId, input),
            CancellationToken.None
        );
        Assert.True(created.IsSuccess);
        await db.SaveChangesAsync(CancellationToken.None);
        return created.Value;
    }

    private static async Task<Slot> LoadSlotAsync(Fixture f, Guid slotId)
    {
        await using var db = f.Db.New();
        return await db.Slots.AsNoTracking().FirstAsync(s => s.Id == slotId);
    }

    private static async Task<int> RevisionAsync(Fixture f)
    {
        await using var db = f.Db.New();
        return (
            await db.ScheduleTemplates.AsNoTracking().FirstAsync(t => t.Id == f.TemplateId)
        ).Revision;
    }

    [Fact]
    public async Task CreateSlot_UnknownLayer_ReturnsLayerNotFound()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new CreateSlotCommandHandler(GroupServices.Slots(db)).Handle(
            new CreateSlotCommand(Guid.NewGuid(), Input(groupId: f.GroupId)),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.LayerNotFound, result.Error);
    }

    [Fact]
    public async Task CreateSlot_StoresTimingAndContent_AndBumpsRevision()
    {
        var f = await SeedAsync();

        var slotId = await AddSlotAsync(f, Input(groupId: f.GroupId, weekday: 3));

        var slot = await LoadSlotAsync(f, slotId);
        Assert.Equal("Вечернее кино", slot.Title);
        Assert.Equal(3, slot.Weekday);
        Assert.Equal(new TimeOnly(20, 0), slot.TargetStart);
        Assert.Equal(120, slot.TargetDurationMinutes);
        Assert.Equal(Daypart.Prime, slot.Daypart);
        Assert.Equal(f.GroupId, slot.GroupId);
        Assert.True(slot.IsAnchor);
        Assert.Equal(15, slot.SnapToMinutes);
        Assert.NotNull(SlotStrategy.FromJson(slot.StrategyJson));
        // Правка сетки эфир не двигает — только помечает шаблон изменённым.
        Assert.Equal(1, await RevisionAsync(f));
    }

    [Fact]
    public async Task CreateSlot_ContentWithoutGroup_ReturnsGroupRequired()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new CreateSlotCommandHandler(GroupServices.Slots(db)).Handle(
            new CreateSlotCommand(f.LayerId, Input(groupId: null)),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.GroupRequired, result.Error);
    }

    [Fact]
    public async Task CreateSlot_UnknownGroup_ReturnsGroupNotFound()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new CreateSlotCommandHandler(GroupServices.Slots(db)).Handle(
            new CreateSlotCommand(f.LayerId, Input(groupId: Guid.NewGuid())),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.GroupNotFound, result.Error);
    }

    [Fact]
    public async Task CreateSlot_RepeatWithoutSource_ReturnsRepeatSourceRequired()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new CreateSlotCommandHandler(GroupServices.Slots(db)).Handle(
            new CreateSlotCommand(f.LayerId, Input(kind: SlotKind.Repeat)),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.RepeatSourceRequired, result.Error);
    }

    [Fact]
    public async Task CreateSlot_Repeat_KeepsSourceAndDropsGroup()
    {
        var f = await SeedAsync();
        var repeat = new RepeatSource(1, new TimeOnly(20, 0), 120);

        var slotId = await AddSlotAsync(
            f,
            Input(kind: SlotKind.Repeat, groupId: f.GroupId, repeat: repeat)
        );

        var slot = await LoadSlotAsync(f, slotId);
        // Группа к повтору отношения не имеет: домен гасит поля чужого типа слота.
        Assert.Null(slot.GroupId);
        Assert.Equal(repeat, RepeatSource.FromJson(slot.RepeatSourceJson));
    }

    [Fact]
    public async Task CreateSlot_OverlappingInSameLayer_ReturnsSlotsOverlap()
    {
        var f = await SeedAsync();
        await AddSlotAsync(f, Input(groupId: f.GroupId));

        await using var db = f.Db.New();
        var result = await new CreateSlotCommandHandler(GroupServices.Slots(db)).Handle(
            new CreateSlotCommand(
                f.LayerId,
                Input(title: "Второй", start: new TimeOnly(21, 0), groupId: f.GroupId)
            ),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.SlotsOverlap, result.Error);
    }

    [Fact]
    public async Task UpdateSlot_UnknownSlot_ReturnsSlotNotFound()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new UpdateSlotCommandHandler(GroupServices.Slots(db)).Handle(
            new UpdateSlotCommand(Guid.NewGuid(), Input(groupId: f.GroupId)),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.SlotNotFound, result.Error);
    }

    [Fact]
    public async Task UpdateSlot_MovesSlot_WithoutCountingItAsOverlap()
    {
        var f = await SeedAsync();
        var slotId = await AddSlotAsync(f, Input(groupId: f.GroupId));

        await using (var db = f.Db.New())
        {
            // Сдвиг внутрь собственного интервала: слот не должен пересечься сам с собой.
            var result = await new UpdateSlotCommandHandler(GroupServices.Slots(db)).Handle(
                new UpdateSlotCommand(
                    slotId,
                    Input(title: "Ночное кино", start: new TimeOnly(21, 0), groupId: f.GroupId)
                ),
                CancellationToken.None
            );
            Assert.True(result.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        var slot = await LoadSlotAsync(f, slotId);
        Assert.Equal("Ночное кино", slot.Title);
        Assert.Equal(new TimeOnly(21, 0), slot.TargetStart);
        Assert.Equal(2, await RevisionAsync(f));
    }

    [Fact]
    public async Task DeleteSlot_RemovesSlot_OrReportsNotFound()
    {
        var f = await SeedAsync();
        var slotId = await AddSlotAsync(f, Input(groupId: f.GroupId));

        await using (var db = f.Db.New())
        {
            var missing = await new DeleteSlotCommandHandler(GroupServices.Slots(db)).Handle(
                new DeleteSlotCommand(Guid.NewGuid()),
                CancellationToken.None
            );
            Assert.Equal(TemplateErrors.SlotNotFound, missing.Error);
        }

        await using (var db = f.Db.New())
        {
            var deleted = await new DeleteSlotCommandHandler(GroupServices.Slots(db)).Handle(
                new DeleteSlotCommand(slotId),
                CancellationToken.None
            );
            Assert.True(deleted.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var verify = f.Db.New();
        Assert.False(await verify.Slots.AnyAsync(s => s.Id == slotId));
    }

    [Fact]
    public async Task CreateLayer_UnknownTemplate_ReturnsNotFound()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new CreateLayerCommandHandler(db).Handle(
            new CreateLayerCommand(Guid.NewGuid(), "Лето", 20),
            CancellationToken.None
        );

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

    [Fact]
    public async Task CreateLayer_AddsLayerToTemplate()
    {
        var f = await SeedAsync();

        Guid layerId;
        await using (var db = f.Db.New())
        {
            var result = await new CreateLayerCommandHandler(db).Handle(
                new CreateLayerCommand(f.TemplateId, "Лето", 20),
                CancellationToken.None
            );
            Assert.True(result.IsSuccess);
            layerId = result.Value;
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var verify = f.Db.New();
        var layer = await verify.GridLayers.AsNoTracking().FirstAsync(l => l.Id == layerId);
        Assert.Equal("Лето", layer.Name);
        Assert.Equal(20, layer.Priority);
        Assert.False(layer.IsBackground);
    }

    [Fact]
    public async Task UpdateLayer_StoresApplicability_OrReportsNotFound()
    {
        var f = await SeedAsync();
        var applicability = new LayerApplicability(Weekdays: [5, 6]);

        await using (var db = f.Db.New())
        {
            var missing = await new UpdateLayerCommandHandler(db).Handle(
                new UpdateLayerCommand(Guid.NewGuid(), "Нет", 5, null, true),
                CancellationToken.None
            );
            Assert.Equal(TemplateErrors.LayerNotFound, missing.Error);
        }

        await using (var db = f.Db.New())
        {
            var result = await new UpdateLayerCommandHandler(db).Handle(
                new UpdateLayerCommand(f.LayerId, "Выходные", 30, applicability, false),
                CancellationToken.None
            );
            Assert.True(result.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var verify = f.Db.New();
        var layer = await verify.GridLayers.AsNoTracking().FirstAsync(l => l.Id == f.LayerId);
        Assert.Equal("Выходные", layer.Name);
        Assert.Equal(30, layer.Priority);
        Assert.False(layer.IsEnabled);
        var stored = LayerApplicability.FromJson(layer.ApplicabilityJson);
        Assert.Equal([5, 6], stored?.Weekdays);
    }

    [Fact]
    public async Task DeleteLayer_RefusesBackground_AndRemovesOrdinary()
    {
        var f = await SeedAsync();

        await using (var db = f.Db.New())
        {
            var background = await new DeleteLayerCommandHandler(db).Handle(
                new DeleteLayerCommand(f.BackgroundLayerId),
                CancellationToken.None
            );
            // Без фонового слоя первую же дыру в сетке нечем закрыть.
            Assert.Equal(TemplateErrors.BackgroundLayerCannotBeDeleted, background.Error);
        }

        await using (var db = f.Db.New())
        {
            var missing = await new DeleteLayerCommandHandler(db).Handle(
                new DeleteLayerCommand(Guid.NewGuid()),
                CancellationToken.None
            );
            Assert.Equal(TemplateErrors.LayerNotFound, missing.Error);
        }

        await using (var db = f.Db.New())
        {
            var deleted = await new DeleteLayerCommandHandler(db).Handle(
                new DeleteLayerCommand(f.LayerId),
                CancellationToken.None
            );
            Assert.True(deleted.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var verify = f.Db.New();
        Assert.False(await verify.GridLayers.AnyAsync(l => l.Id == f.LayerId));
    }

    [Fact]
    public async Task UpdateTemplate_UnknownTemplate_ReturnsNotFound()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new UpdateTemplateCommandHandler(
            db,
            GroupServices.Interstitials(db)
        ).Handle(
            new UpdateTemplateCommand(Guid.NewGuid(), "Сетка", null, null, null),
            CancellationToken.None
        );

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

    [Fact]
    public async Task UpdateTemplate_UnknownFallbackGroup_ReturnsGroupNotFound()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new UpdateTemplateCommandHandler(
            db,
            GroupServices.Interstitials(db)
        ).Handle(
            new UpdateTemplateCommand(f.TemplateId, "Сетка", Guid.NewGuid(), null, null),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.GroupNotFound, result.Error);
    }

    [Fact]
    public async Task UpdateTemplate_StoresFallbackAndRules()
    {
        var f = await SeedAsync();
        var rules = new PlanningRules(
            MaxAudienceByTime:
            [
                new AudienceWindow(
                    new TimeOnly(6, 0),
                    new TimeOnly(21, 0),
                    Domain.Library.ShowAudience.Pg
                ),
            ],
            MaxRepeatsInWindow: new RepeatLimitRule(7, 2)
        );

        await using (var db = f.Db.New())
        {
            var result = await new UpdateTemplateCommandHandler(
                db,
                GroupServices.Interstitials(db)
            ).Handle(
                new UpdateTemplateCommand(f.TemplateId, "  Новая сетка  ", f.GroupId, null, rules),
                CancellationToken.None
            );
            Assert.True(result.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var verify = f.Db.New();
        var template = await verify
            .ScheduleTemplates.AsNoTracking()
            .FirstAsync(t => t.Id == f.TemplateId);
        Assert.Equal("Новая сетка", template.Name);
        Assert.Equal(f.GroupId, template.FallbackGroupId);
        var storedRules = PlanningRules.FromJson(template.RulesJson);
        Assert.Equal(rules.MaxRepeatsInWindow, storedRules?.MaxRepeatsInWindow);
        Assert.Equal(rules.MaxAudienceByTime, storedRules?.MaxAudienceByTime);
        Assert.True(template.HasPendingChanges);
    }

    [Fact]
    public async Task GetChannelTemplate_UnknownChannel_ReturnsNotFound()
    {
        var f = await SeedAsync();
        await using var db = f.Db.New();

        var result = await new GetChannelTemplateQueryHandler(db).Handle(
            new GetChannelTemplateQuery(Guid.NewGuid()),
            CancellationToken.None
        );

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

    [Fact]
    public async Task GetChannelTemplate_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 GetChannelTemplateQueryHandler(db).Handle(
            new GetChannelTemplateQuery(channel.Id),
            CancellationToken.None
        );

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

    [Fact]
    public async Task GetChannelTemplate_OrdersLayersByPriority_AndResolvesGroupNames()
    {
        var f = await SeedAsync();
        var slotId = await AddSlotAsync(f, Input(groupId: f.GroupId));

        await using var db = f.Db.New();
        var result = await new GetChannelTemplateQueryHandler(db).Handle(
            new GetChannelTemplateQuery(f.ChannelId),
            CancellationToken.None
        );

        Assert.True(result.IsSuccess);
        var dto = result.Value;
        Assert.Equal(f.TemplateId, dto.Id);
        Assert.True(dto.HasPendingChanges);
        // Приоритетный слой идёт первым, фоновый — последним.
        Assert.Equal(
            new[] { f.LayerId, f.BackgroundLayerId },
            dto.Layers.Select(l => l.Id).ToList()
        );
        Assert.True(dto.Layers[^1].IsBackground);

        var slot = Assert.Single(dto.Layers[0].Slots);
        Assert.Equal(slotId, slot.Id);
        Assert.Equal("Кино", slot.GroupName);
        Assert.Equal(SlotStrategyType.Sequential, slot.Strategy?.Type);
    }
}
