using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.CopyTemplate;
using TeleWave.Application.Programming.Templates.CreateTemplate;
using TeleWave.Application.Programming.Templates.Reset;
using TeleWave.Application.Programming.Templates.Validate;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Programming;
using TeleWave.Infrastructure.Persistence;
using Xunit;

namespace TeleWave.Integration.Tests;

/// <summary>
/// Операции над шаблоном против настоящей БД: копирование на другой канал и проверки по правилам.
/// Обе штуки — сплошные запросы EF и перекладывание графов, в юнит-тестах их не поймать.
/// </summary>
[Collection("postgres")]
public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
{
    [SkippableFact]
    public async Task Copy_MovesLayersAndSlots_AndReplacesTargetGrid()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");

        await using var seedDb = fixture.CreateContext();
        var (sourceId, targetId, groupId) = await SeedPairAsync(seedDb);

        await using var db = fixture.CreateContext();
        var result = await new CopyTemplateCommandHandler(db).Handle(
            new CopyTemplateCommand(sourceId, targetId),
            default
        );
        Assert.True(result.IsSuccess);
        await db.SaveChangesAsync();

        Assert.Equal(1, result.Value.Layers);
        Assert.Equal(1, result.Value.Slots);

        await using var verify = fixture.CreateContext();
        var target = verify.Channels.Single(c => c.Id == targetId);
        var copied = verify
            .ScheduleTemplates.Include(t => t.Layers)
                .ThenInclude(l => l.Slots)
            .Single(t => t.ChannelId == targetId);

        // У приёмника ровно один шаблон, и канал смотрит именно на него.
        Assert.Equal(copied.Id, target.TemplateId);
        Assert.Single(verify.ScheduleTemplates.Where(t => t.ChannelId == targetId));

        var slot = copied.Layers.SelectMany(l => l.Slots).Single();
        // Группы общие — ссылка переносится как есть, а не копией группы.
        Assert.Equal(groupId, slot.GroupId);
        Assert.Single(verify.Groups.Where(g => g.Id == groupId));

        // Стыки общие — копия ссылается на тот же стык, а не заводит свой.
        var sourceSlot = verify
            .ScheduleTemplates.Include(t => t.Layers)
                .ThenInclude(l => l.Slots)
            .Single(t => t.ChannelId == sourceId)
            .Layers.SelectMany(l => l.Slots)
            .Single();
        Assert.Equal(sourceSlot.JunctionAfterId, slot.JunctionAfterId);
        Assert.NotNull(slot.JunctionAfterId);
    }

    [Fact]
    public void Copy_ToItself_IsRejectedByValidator()
    {
        var validator = new CopyTemplateCommandValidator();
        var id = Guid.NewGuid();

        Assert.False(validator.Validate(new CopyTemplateCommand(id, id)).IsValid);
        Assert.True(validator.Validate(new CopyTemplateCommand(id, Guid.NewGuid())).IsValid);
    }

    [SkippableFact]
    public async Task Validate_ReportsEmptyGroupAndGridGap()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");

        await using var seedDb = fixture.CreateContext();
        var (sourceId, _, _) = await SeedPairAsync(seedDb);

        await using var db = fixture.CreateContext();
        var result = await new ValidateTemplateQueryHandler(
            db,
            new DynamicGroupResolver(new GroupFilterMatcher(db), new GroupElementResolver(db))
        ).Handle(new ValidateTemplateQuery(sourceId), default);

        Assert.True(result.IsSuccess);
        // Группа в сиде пустая, а слот занимает лишь два часа суток — обе проверки должны сработать.
        Assert.Contains(result.Value, i => i.Kind == TemplateIssueKind.GroupEmpty);
        Assert.Contains(result.Value, i => i.Kind == TemplateIssueKind.GridGap);
    }

    [SkippableFact]
    public async Task CreateTemplate_ForChannelWithoutGrid_LinksItAndIsIdempotent()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");

        // Канал из старой ротации: сетки нет, ссылки на неё тоже.
        await using var seedDb = fixture.CreateContext();
        var suffix = Guid.NewGuid().ToString("N")[..8];
        var channel = Channel.Create(
            $"Без сетки {suffix}",
            $"nogrid-{suffix}",
            DateTimeOffset.UtcNow
        );
        seedDb.Channels.Add(channel);
        await seedDb.SaveChangesAsync();

        await using var db = fixture.CreateContext();
        var created = await new CreateChannelTemplateCommandHandler(db).Handle(
            new CreateChannelTemplateCommand(channel.Id),
            default
        );
        Assert.True(created.IsSuccess);
        await db.SaveChangesAsync();

        await using var again = fixture.CreateContext();
        var second = await new CreateChannelTemplateCommandHandler(again).Handle(
            new CreateChannelTemplateCommand(channel.Id),
            default
        );
        await again.SaveChangesAsync();

        // Повторный вызов возвращает ту же сетку: одна на канал, второй не появляется.
        Assert.True(second.IsSuccess);
        Assert.Equal(created.Value, second.Value);

        await using var verify = fixture.CreateContext();
        var template = verify.ScheduleTemplates.Single(t => t.ChannelId == channel.Id);
        Assert.Equal(template.Id, verify.Channels.Single(c => c.Id == channel.Id).TemplateId);
        // Фоновый слой заводится сразу — без него первую же дыру в сетке нечем закрыть.
        Assert.Single(verify.GridLayers.Where(l => l.TemplateId == template.Id && l.IsBackground));
    }

    /// <summary>
    /// Сброс: сетка снимается целиком, будущий эфир уходит, прошлое и идущая запись остаются.
    /// Проверяется на настоящей БД — команда держит транзакцию с advisory-локом канала и сносит
    /// ленту через ExecuteDelete, а этого InMemory не умеет.
    /// </summary>
    [SkippableFact]
    public async Task Reset_ClearsGridAndFutureTape_KeepingPastAndCurrentEntry()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");

        await using var seedDb = fixture.CreateContext();
        var (channelId, _, _) = await SeedPairAsync(seedDb);

        var now = DateTimeOffset.UtcNow;
        var slotId = seedDb
            .ScheduleTemplates.Include(t => t.Layers)
                .ThenInclude(l => l.Slots)
            .Single(t => t.ChannelId == channelId)
            .Layers.SelectMany(l => l.Slots)
            .Single()
            .Id;

        // Курсор слота: он обязан уйти вместе со слотом, иначе сериал продолжится с той же серии.
        var state = SlotState.Create(slotId);
        state.MoveTo(GroupElementKind.Show, Guid.NewGuid(), 3);
        seedDb.SlotStates.Add(state);

        Entry(seedDb, channelId, now.AddHours(-2), now.AddHours(-1));
        Entry(seedDb, channelId, now.AddMinutes(-10), now.AddMinutes(20));
        Entry(seedDb, channelId, now.AddHours(1), now.AddHours(2));
        await seedDb.SaveChangesAsync();

        await using var db = fixture.CreateContext();
        var result = await new ResetTemplateCommandHandler(db).Handle(
            new ResetTemplateCommand(channelId),
            default
        );

        Assert.True(result.IsSuccess);
        Assert.Equal(1, result.Value.Slots);
        Assert.Equal(1, result.Value.Layers);
        Assert.Equal(1, result.Value.Entries);

        await using var verify = fixture.CreateContext();
        var template = verify
            .ScheduleTemplates.Include(t => t.Layers)
                .ThenInclude(l => l.Slots)
            .Single(t => t.ChannelId == channelId);

        // Фоновый слой остаётся всегда — удалить его нельзя, и без него сетку нечем закрывать.
        var layer = Assert.Single(template.Layers);
        Assert.True(layer.IsBackground);
        Assert.Empty(layer.Slots);
        Assert.True(template.HasPendingChanges);
        Assert.False(verify.SlotStates.Any(s => s.SlotId == slotId));

        var entries = verify.ScheduleEntries.Where(e => e.ChannelId == channelId).ToList();
        Assert.Equal(2, entries.Count);
        Assert.All(entries, e => Assert.True(e.StartsAtUtc < now));
    }

    private static void Entry(
        AppDbContext db,
        Guid channelId,
        DateTimeOffset from,
        DateTimeOffset to
    ) =>
        db.ScheduleEntries.Add(
            ScheduleEntry.FromSlot(
                channelId,
                Guid.NewGuid(),
                ScheduleEntryKind.Program,
                from,
                to,
                new ScheduleEntryOrigin(null, null, null, null, null)
            )
        );

    /// <summary>Два канала: у источника слой со слотом, группой и стыком; у приёмника — пустая сетка.</summary>
    private static async Task<(Guid Source, Guid Target, Guid GroupId)> SeedPairAsync(
        AppDbContext db
    )
    {
        var suffix = Guid.NewGuid().ToString("N")[..8];

        var group = Group.Create($"Группа {suffix}");
        db.Groups.Add(group);

        var source = Channel.Create($"Источник {suffix}", $"src-{suffix}", DateTimeOffset.UtcNow);
        var target = Channel.Create($"Приёмник {suffix}", $"dst-{suffix}", DateTimeOffset.UtcNow);

        var junction = JunctionTemplate.Create($"Прайм {suffix}");
        var ad = junction.AddElement(JunctionElementKind.Ad);
        ad.Update(
            new JunctionElementSettings(
                JunctionElementKind.Ad,
                group.Id,
                null,
                null,
                JunctionAmountMode.Count,
                2,
                true,
                null,
                JunctionElement.DefaultChoiceWeight,
                null
            )
        );
        db.JunctionTemplates.Add(junction);

        var sourceTemplate = ScheduleTemplate.Create(source.Id, "Сетка источника");
        var layer = sourceTemplate.AddLayer("Прайм", 10);
        var slot = layer.AddSlot("Вечернее кино", new TimeOnly(20, 0), 120);
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                SlotKind.Content,
                group.Id,
                new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext,
                JunctionAfterId: junction.Id
            )
        );
        sourceTemplate.SetDefaultJunction(junction.Id);
        source.SetTemplate(sourceTemplate.Id);

        var targetTemplate = ScheduleTemplate.Create(target.Id, "Сетка приёмника");
        target.SetTemplate(targetTemplate.Id);

        db.Channels.AddRange(source, target);
        db.ScheduleTemplates.AddRange(sourceTemplate, targetTemplate);

        await db.SaveChangesAsync();
        return (source.Id, target.Id, group.Id);
    }
}
