using Microsoft.EntityFrameworkCore;
using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Library.DeleteShow;
using TeleWave.Application.Maintenance.DeleteShowMedia;
using TeleWave.Application.Media;
using TeleWave.Application.Media.Register;
using TeleWave.Application.Programming.Groups;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using Xunit;

namespace TeleWave.Integration.Tests;

[Collection("postgres")]
public sealed class TransactionIntegrationTests(PostgresFixture fixture)
{
    [SkippableFact]
    public async Task RegisterMediaAsset_RollsBack_WhenFilePromoteFails()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");
        var fileName = $"clip-{Guid.NewGuid():N}.mkv";
        var storage = Substitute.For<IMediaStorage>();
        storage
            .PromoteToOriginalAsync(
                Arg.Any<MediaSource>(),
                Arg.Any<string>(),
                Arg.Any<Guid>(),
                Arg.Any<string>(),
                Arg.Any<CancellationToken>()
            )
            .Returns(Task.FromException(new IOException("disk full")));

        await using var db = fixture.CreateContext();
        var handler = new RegisterMediaAssetCommandHandler(db, storage);

        await Assert.ThrowsAsync<IOException>(() =>
            handler.Handle(
                new RegisterMediaAssetCommand(fileName, MediaSource.Upload, fileName),
                CancellationToken.None
            )
        );

        // Перенос файла упал — регистрация должна откатиться, чтобы не осталось orphan-строки.
        await using var verify = fixture.CreateContext();
        var exists = await verify.MediaAssets.AnyAsync(a => a.OriginalFileName == fileName);
        Assert.False(exists);
    }

    [SkippableFact]
    public async Task DeleteShowMedia_AtomicallyRemovesEntriesAssetsAndEpisodes()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");
        var channel = Channel.Create("c", $"c-{Guid.NewGuid():N}", DateTimeOffset.UnixEpoch);
        var show = Show.Create("Show", ShowKind.Series);
        var asset = MediaAsset.Register("ep.mkv", ".mkv", MediaSource.Upload);
        asset.MarkReady(
            new MediaReadyInfo(
                TimeSpan.FromMinutes(20),
                2,
                600,
                1920,
                1080,
                "h264",
                "aac",
                "assets/x"
            )
        );
        show.AddEpisode(asset.Id);
        var entry = ScheduleEntry.Program(
            channel.Id,
            asset.Id,
            DateTimeOffset.UnixEpoch,
            DateTimeOffset.UnixEpoch.AddMinutes(20),
            show.Id,
            0
        );

        await using (var seed = fixture.CreateContext())
        {
            seed.Channels.Add(channel);
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            seed.ScheduleEntries.Add(entry);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var storage = Substitute.For<IMediaStorage>();
        storage
            .DeleteAssetArtifactsAsync(
                Arg.Any<Guid>(),
                Arg.Any<string>(),
                Arg.Any<CancellationToken>()
            )
            .Returns(Task.CompletedTask);

        await using (var db = fixture.CreateContext())
        {
            var result = await new DeleteShowMediaCommandHandler(
                db,
                new ShowMediaEraser(db, storage)
            ).Handle(new DeleteShowMediaCommand(show.Id), CancellationToken.None);
            Assert.True(result.IsSuccess);
        }

        await using var verify = fixture.CreateContext();
        Assert.False(await verify.MediaAssets.AnyAsync(a => a.Id == asset.Id));
        Assert.False(await verify.ScheduleEntries.AnyAsync(e => e.MediaAssetId == asset.Id));
        var reloaded = await verify.Shows.Include(s => s.Episodes).FirstAsync(s => s.Id == show.Id);
        Assert.Empty(reloaded.Episodes);
        await storage
            .Received()
            .DeleteAssetArtifactsAsync(asset.Id, ".mkv", Arg.Any<CancellationToken>());
    }

    /// <summary>
    /// Шоу, стоящее в будущем эфире, удаляется с флагом «вырезать из эфира»: пересборка канала
    /// спасает не всегда — идущую запись она не трогает, а выключенный канал вещать не начнёт.
    /// </summary>
    [SkippableFact]
    public async Task DeleteShow_WithSchedule_CutsFutureEntries()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");
        var channel = Channel.Create("Канал", $"cut-{Guid.NewGuid():N}", DateTimeOffset.UnixEpoch);
        var show = Show.Create("Шоу", ShowKind.Single);
        var asset = MediaAsset.Register("live.mkv", ".mkv", MediaSource.Upload);
        show.AddEpisode(asset.Id);

        // Запись идёт прямо сейчас: пересборка её не удаляет, а значит и шоу не освобождает.
        var running = ScheduleEntry.Program(
            channel.Id,
            asset.Id,
            DateTimeOffset.UtcNow.AddMinutes(-5),
            DateTimeOffset.UtcNow.AddMinutes(15),
            show.Id,
            0
        );

        await using (var seed = fixture.CreateContext())
        {
            seed.Channels.Add(channel);
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            seed.ScheduleEntries.Add(running);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using (var db = fixture.CreateContext())
        {
            var result = await new DeleteShowCommandHandler(
                db,
                new GroupMembershipCleaner(
                    db,
                    new GroupStatsService(
                        db,
                        new GroupElementResolver(db),
                        new DynamicGroupResolver(
                            new GroupFilterMatcher(db),
                            new GroupElementResolver(db)
                        )
                    )
                ),
                new ShowMediaEraser(db, Substitute.For<IMediaStorage>())
            ).Handle(new DeleteShowCommand(show.Id, WithSchedule: true), CancellationToken.None);

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

        await using var verify = fixture.CreateContext();
        Assert.False(await verify.Shows.AnyAsync(s => s.Id == show.Id));
        Assert.False(await verify.ScheduleEntries.AnyAsync(e => e.ShowId == show.Id));
        // Медиа не просили — оно осталось.
        Assert.True(await verify.MediaAssets.AnyAsync(a => a.Id == asset.Id));
    }

    /// <summary>
    /// Удаление шоу «вместе с медиа»: ассеты, записи расписания и файлы уходят одной операцией.
    /// Проверяется на настоящем Postgres — <c>ExecuteDelete</c> и транзакции InMemory не умеет.
    /// </summary>
    [SkippableFact]
    public async Task DeleteShow_WithMedia_RemovesEverything()
    {
        Skip.IfNot(fixture.Available, "Docker недоступен");
        var channel = Channel.Create("Канал", $"del-{Guid.NewGuid():N}", DateTimeOffset.UnixEpoch);
        var show = Show.Create("Шоу", ShowKind.Single);
        var asset = MediaAsset.Register("movie.mkv", ".mkv", MediaSource.Upload);
        show.AddEpisode(asset.Id);
        var entry = ScheduleEntry.Program(
            channel.Id,
            asset.Id,
            DateTimeOffset.UnixEpoch,
            DateTimeOffset.UnixEpoch.AddMinutes(20),
            show.Id,
            0
        );

        await using (var seed = fixture.CreateContext())
        {
            seed.Channels.Add(channel);
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            seed.ScheduleEntries.Add(entry);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var storage = Substitute.For<IMediaStorage>();

        await using (var db = fixture.CreateContext())
        {
            var result = await new DeleteShowCommandHandler(
                db,
                new GroupMembershipCleaner(
                    db,
                    new GroupStatsService(
                        db,
                        new GroupElementResolver(db),
                        new DynamicGroupResolver(
                            new GroupFilterMatcher(db),
                            new GroupElementResolver(db)
                        )
                    )
                ),
                new ShowMediaEraser(db, storage)
            ).Handle(new DeleteShowCommand(show.Id, WithMedia: true), CancellationToken.None);

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

        await using var verify = fixture.CreateContext();
        Assert.False(await verify.Shows.AnyAsync(s => s.Id == show.Id));
        Assert.False(await verify.MediaAssets.AnyAsync(a => a.Id == asset.Id));
        Assert.False(await verify.ScheduleEntries.AnyAsync(e => e.MediaAssetId == asset.Id));
        await storage
            .Received()
            .DeleteAssetArtifactsAsync(asset.Id, ".mkv", Arg.Any<CancellationToken>());
    }
}
