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));

    [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 future = DateTimeOffset.UtcNow.AddHours(1);

        await using (var seed = fixture.New())
        {
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            seed.ScheduleEntries.Add(
                ScheduleEntry.Program(
                    Guid.NewGuid(),
                    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)).Handle(
            new DeleteShowCommand(show.Id),
            CancellationToken.None
        );

        Assert.False(result.IsSuccess);
        Assert.Equal(ShowErrors.InFutureSchedule, result.Error);
    }

    /// <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)).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, result.Error);
        // Файлы не тронуты: до чистки хранилища дело не дошло.
        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, bumpersEnabled: false, 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, result.Error);
    }

    [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, result.Error);
    }

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

        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, result.Error);
    }

    [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);
    }
}
