using Microsoft.EntityFrameworkCore;
using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Library;
using TeleWave.Application.Library.DeleteShow;
using TeleWave.Application.Media;
using TeleWave.Application.Media.Delete;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Groups.DeleteGroup;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using Xunit;

namespace TeleWave.Application.Tests.Library;

/// <summary>
/// Удаление занятой сущности: ссылки на шоу, ассет и группу живут в таблицах без внешних ключей
/// (или с Restrict), поэтому БД такое удаление либо пропустит с висячей ссылкой, либо уронит
/// исключением. Проверяем, что до неё дело не доходит — возвращается управляемая ошибка.
/// </summary>
public class DeleteGuardsTests
{
    private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);

    private static GroupMembershipCleaner GroupCleaner(IAppDbContext db) =>
        new(db, GroupServices.Stats(db));

    private static ShowMediaEraser Eraser(IAppDbContext db) =>
        new(db, Substitute.For<IMediaStorage>());

    [Fact]
    public async Task DeleteShow_InFutureSchedule_Fails()
    {
        var fixture = new TestDb();
        var show = Show.Create("A", ShowKind.Series);
        var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);
        var channel = Channel.Create("Первый", "first", T0);
        var future = DateTimeOffset.UtcNow.AddHours(1);

        await using (var seed = fixture.New())
        {
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            seed.Channels.Add(channel);
            seed.ScheduleEntries.Add(
                ScheduleEntry.Program(
                    channel.Id,
                    asset.Id,
                    future,
                    future.AddMinutes(20),
                    show.Id,
                    0
                )
            );
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var result = await new DeleteShowCommandHandler(db, GroupCleaner(db), Eraser(db)).Handle(
            new DeleteShowCommand(show.Id),
            CancellationToken.None
        );

        Assert.False(result.IsSuccess);
        Assert.Equal(ShowErrors.InFutureSchedule(string.Empty).Code, result.Error.Code);
        // В тексте — канал и время: по ним видно, что именно пересобирать.
        Assert.Contains("Первый", result.Error.Message, StringComparison.Ordinal);
    }

    /// <summary>
    /// Без флага медиа переживает шоу: серия — это ссылка на ассет по значению, внешнего ключа
    /// между ними нет, и каскад БД до файлов не доходит.
    /// </summary>
    [Fact]
    public async Task DeleteShow_KeepsMedia_ByDefault()
    {
        var fixture = new TestDb();
        var show = Show.Create("A", ShowKind.Single);
        var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);
        show.AddEpisode(asset.Id);

        await using (var seed = fixture.New())
        {
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using (var db = fixture.New())
        {
            var result = await new DeleteShowCommandHandler(
                db,
                GroupCleaner(db),
                Eraser(db)
            ).Handle(new DeleteShowCommand(show.Id), CancellationToken.None);
            Assert.True(result.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var check = fixture.New();
        Assert.Empty(await check.Shows.ToListAsync(CancellationToken.None));
        Assert.Single(await check.MediaAssets.ToListAsync(CancellationToken.None));
    }

    /// <summary>Отыгранный эфир шоу не держит: иначе его нельзя было бы удалить никогда.</summary>
    [Fact]
    public async Task DeleteShow_OnlyPastSchedule_Succeeds()
    {
        var fixture = new TestDb();
        var show = Show.Create("A", ShowKind.Series);
        var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);

        await using (var seed = fixture.New())
        {
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            seed.ScheduleEntries.Add(
                ScheduleEntry.Program(Guid.NewGuid(), asset.Id, T0, T0.AddMinutes(20), show.Id, 0)
            );
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var result = await new DeleteShowCommandHandler(db, GroupCleaner(db), Eraser(db)).Handle(
            new DeleteShowCommand(show.Id),
            CancellationToken.None
        );

        Assert.True(result.IsSuccess);
    }

    [Fact]
    public async Task DeleteMediaAsset_InSchedule_Fails()
    {
        var fixture = new TestDb();
        var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);

        await using (var seed = fixture.New())
        {
            seed.MediaAssets.Add(asset);
            seed.ScheduleEntries.Add(
                ScheduleEntry.Ad(Guid.NewGuid(), asset.Id, T0, T0.AddMinutes(1))
            );
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var storage = Substitute.For<IMediaStorage>();
        await using var db = fixture.New();
        var result = await new DeleteMediaAssetCommandHandler(db, storage).Handle(
            new DeleteMediaAssetCommand(asset.Id),
            CancellationToken.None
        );

        Assert.False(result.IsSuccess);
        Assert.Equal(MediaErrors.InUse(string.Empty).Code, result.Error.Code);
        // Файлы не тронуты: до чистки хранилища дело не дошло.
        await storage
            .DidNotReceiveWithAnyArgs()
            .DeleteAssetArtifactsAsync(default, default!, default);
    }

    [Fact]
    public async Task DeleteMediaAsset_ChannelFiller_Fails()
    {
        var fixture = new TestDb();
        var asset = MediaAsset.Register("filler.mkv", ".mkv", MediaSource.Upload);
        var channel = Channel.Create("c", "c", T0);
        channel.UpdateSettings(channel.Name, isEnabled: true, asset.Id);

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

        await using var db = fixture.New();
        var result = await new DeleteMediaAssetCommandHandler(
            db,
            Substitute.For<IMediaStorage>()
        ).Handle(new DeleteMediaAssetCommand(asset.Id), CancellationToken.None);

        Assert.False(result.IsSuccess);
        Assert.Equal(MediaErrors.InUse(string.Empty).Code, result.Error.Code);
    }

    [Fact]
    public async Task DeleteGroup_UsedBySlot_Fails()
    {
        var fixture = new TestDb();
        var group = Group.Create("G");
        var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
        var layer = template.AddLayer("Базовый", 10);
        var slot = layer.AddSlot("Блок", new TimeOnly(6, 0), 60);
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                SlotKind.Content,
                group.Id,
                null,
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext
            )
        );

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

        await using var db = fixture.New();
        var result = await new DeleteGroupCommandHandler(db).Handle(
            new DeleteGroupCommand(group.Id),
            CancellationToken.None
        );

        Assert.False(result.IsSuccess);
        Assert.Equal(GroupErrors.InUse(string.Empty).Code, result.Error.Code);
    }

    /// <summary>
    /// Запасная группа шаблона удаление не держит: слот и врезка — осознанная настройка сетки,
    /// а её проставляет и автосборка, и снять её из интерфейса было нечем — группа оставалась
    /// неудаляемой навсегда. Ссылка снимается, ревизия растёт: эфиру нужна пересборка.
    /// </summary>
    [Fact]
    public async Task DeleteGroup_UsedAsTemplateFallback_ReleasesReference()
    {
        var fixture = new TestDb();
        var group = Group.Create("G");
        var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
        template.SetFallbackGroup(group.Id);
        template.MarkApplied();

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

        await using (var db = fixture.New())
        {
            var result = await new DeleteGroupCommandHandler(db).Handle(
                new DeleteGroupCommand(group.Id),
                CancellationToken.None
            );

            Assert.True(result.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var check = fixture.New();
        var stored = await check.ScheduleTemplates.FirstAsync(t => t.Id == template.Id);
        Assert.Null(stored.FallbackGroupId);
        Assert.True(stored.HasPendingChanges);
        Assert.False(await check.Groups.AnyAsync(g => g.Id == group.Id));
    }

    /// <summary>
    /// Отказ должен называть адрес: «используется в сетке» без имени канала отправляет обходить
    /// их все руками. Связи полные — значит и канал в тексте есть.
    /// </summary>
    [Fact]
    public async Task DeleteGroup_UsedByChannel_NamesIt()
    {
        var fixture = new TestDb();
        var group = Group.Create("G");
        var channel = Channel.Create("Первый", $"first-{Guid.NewGuid():N}", T0);
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        var layer = template.AddLayer("Базовый", 10);
        var slot = layer.AddSlot("Блок", new TimeOnly(6, 0), 60);
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                SlotKind.Content,
                group.Id,
                null,
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext
            )
        );

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

        await using var db = fixture.New();
        var result = await new DeleteGroupCommandHandler(db).Handle(
            new DeleteGroupCommand(group.Id),
            CancellationToken.None
        );

        Assert.False(result.IsSuccess);
        Assert.Contains("Первый", result.Error.Message, StringComparison.Ordinal);
    }

    [Fact]
    public async Task DeleteGroup_Unreferenced_Succeeds()
    {
        var fixture = new TestDb();
        var group = Group.Create("G");

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

        await using var db = fixture.New();
        var result = await new DeleteGroupCommandHandler(db).Handle(
            new DeleteGroupCommand(group.Id),
            CancellationToken.None
        );

        Assert.True(result.IsSuccess);
    }
}
