using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.Restore;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Programming;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

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

    /// <summary>Канал с применённой сеткой: один слот и снятый по нему снимок.</summary>
    private static async Task<(Guid ChannelId, Guid SlotId)> SeedAppliedAsync(
        TestDb fixture,
        Guid? groupId = null
    )
    {
        var channel = Channel.Create("Первый", "first", T0);
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        channel.SetTemplate(template.Id);

        var layer = template.AddLayer("Основная сетка", 10);
        var slot = layer.AddSlot("Вечернее кино", new TimeOnly(20, 0), 120);
        slot.UpdateContent(
            new SlotContent(
                "Вечернее кино",
                SlotKind.Content,
                groupId,
                null,
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext
            )
        );

        // Применение снимает слепок — именно к нему потом откатываемся.
        template.MarkApplied(TemplateSnapshot.Capture(template).ToJson());

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

        return (channel.Id, slot.Id);
    }

    private static async Task<ScheduleTemplate> LoadAsync(TestDb fixture)
    {
        await using var db = fixture.New();
        return await db
            .ScheduleTemplates.AsNoTracking()
            .Include(t => t.Layers)
                .ThenInclude(l => l.Slots)
            .AsSplitQuery()
            .FirstAsync(CancellationToken.None);
    }

    private static async Task<RestoreTemplateResultDto> RestoreAsync(TestDb fixture, Guid channelId)
    {
        await using var db = fixture.New();
        var result = await new RestoreTemplateCommandHandler(db).Handle(
            new RestoreTemplateCommand(channelId),
            CancellationToken.None
        );
        Assert.True(result.IsSuccess);
        await db.SaveChangesAsync(CancellationToken.None);
        return result.Value;
    }

    [Fact]
    public async Task RevertsEditedSlot_KeepingItsIdentity()
    {
        var fixture = new TestDb();
        var (channelId, slotId) = await SeedAppliedAsync(fixture);

        await using (var db = fixture.New())
        {
            var template = await db
                .ScheduleTemplates.Include(t => t.Layers)
                    .ThenInclude(l => l.Slots)
                .FirstAsync(CancellationToken.None);
            var slot = template.Layers.SelectMany(l => l.Slots).Single();
            slot.UpdateTiming(3, new TimeOnly(8, 0), 30, Daypart.Morning, false, 5, null);
            template.MarkChanged();
            await db.SaveChangesAsync(CancellationToken.None);
        }

        var result = await RestoreAsync(fixture, channelId);

        Assert.Equal(1, result.Slots);
        var restored = (await LoadAsync(fixture)).Layers.SelectMany(l => l.Slots).Single();
        Assert.Equal(new TimeOnly(20, 0), restored.TargetStart);
        Assert.Equal(120, restored.TargetDurationMinutes);
        Assert.Null(restored.Weekday);
        // Тот же слот, а не копия: с его идентификатором связано состояние (курсор сериала).
        Assert.Equal(slotId, restored.Id);
    }

    [Fact]
    public async Task RemovesWhatAppearedAfterApply_AndBringsBackWhatWasDeleted()
    {
        var fixture = new TestDb();
        var (channelId, slotId) = await SeedAppliedAsync(fixture);

        await using (var db = fixture.New())
        {
            var template = await db
                .ScheduleTemplates.Include(t => t.Layers)
                    .ThenInclude(l => l.Slots)
                .FirstAsync(CancellationToken.None);
            var layer = template.Layers.Single(l => !l.IsBackground);
            layer.RemoveSlot(slotId);
            layer.AddSlot("Лишний", new TimeOnly(10, 0), 60);
            template.AddLayer("Лишний слой", 20);
            template.MarkChanged();
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await RestoreAsync(fixture, channelId);

        var restored = await LoadAsync(fixture);
        var slot = Assert.Single(restored.Layers.SelectMany(l => l.Slots));
        Assert.Equal("Вечернее кино", slot.Title);
        Assert.DoesNotContain(restored.Layers, l => l.Name == "Лишний слой");
        // Удалённый слот возвращается новым: его состояние ушло вместе с ним, вернуть нечего.
        Assert.NotEqual(slotId, slot.Id);
    }

    [Fact]
    public async Task ClearsPendingFlag()
    {
        var fixture = new TestDb();
        var (channelId, _) = await SeedAppliedAsync(fixture);

        await using (var db = fixture.New())
        {
            var template = await db.ScheduleTemplates.FirstAsync(CancellationToken.None);
            template.SetRules("{\"maxBreakMinutesPerHour\":10}");
            template.MarkChanged();
            await db.SaveChangesAsync(CancellationToken.None);
            Assert.True(template.HasPendingChanges);
        }

        await RestoreAsync(fixture, channelId);

        var restored = await LoadAsync(fixture);
        Assert.False(restored.HasPendingChanges);
        Assert.Null(restored.RulesJson);
    }

    [Fact]
    public async Task DropsReferenceToGroupDeletedAfterApply()
    {
        var fixture = new TestDb();
        var group = Group.Create("Кино");
        Guid channelId;

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

        (channelId, _) = await SeedAppliedAsync(fixture, group.Id);

        // Слот удалили, следом удалили и группу — ссылка в снимке осталась висеть.
        await using (var db = fixture.New())
        {
            var template = await db
                .ScheduleTemplates.Include(t => t.Layers)
                    .ThenInclude(l => l.Slots)
                .FirstAsync(CancellationToken.None);
            var layer = template.Layers.Single(l => !l.IsBackground);
            layer.RemoveSlot(layer.Slots[0].Id);
            db.Groups.Remove(await db.Groups.FirstAsync(CancellationToken.None));
            template.MarkChanged();
            await db.SaveChangesAsync(CancellationToken.None);
        }

        var result = await RestoreAsync(fixture, channelId);

        // Откат не падает из-за прибранной группы: слот возвращается без неё, а потеря сосчитана.
        Assert.Equal(1, result.DroppedRefs);
        var slot = (await LoadAsync(fixture)).Layers.SelectMany(l => l.Slots).Single();
        Assert.Null(slot.GroupId);
    }

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

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

        await using var db = fixture.New();
        var result = await new RestoreTemplateCommandHandler(db).Handle(
            new RestoreTemplateCommand(channel.Id),
            CancellationToken.None
        );

        Assert.False(result.IsSuccess);
        Assert.Equal(TemplateErrors.NothingToRestore, result.Error);
    }
}
