using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using LanguageExt;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Channel = System.Threading.Channels.Channel;

namespace ErsatzTV.Tests.Application.Subtitles;

[TestFixture]
public class ExtractEmbeddedSubtitlesHandlerTests
{
    private InMemoryTvContext _db = null!;
    private ChannelWriter<IBackgroundServiceRequest> _worker = null!;
    private IEntityLocker _entityLocker = null!;
    private IConfigElementRepository _configRepo = null!;
    private readonly List<string> _tempFiles = [];
    private int _channelNumber = 100;

    [SetUp]
    public async Task SetUp()
    {
        _db = await InMemoryTvContext.CreateAsync();
        _worker = Channel.CreateUnbounded<IBackgroundServiceRequest>().Writer;
        _entityLocker = Substitute.For<IEntityLocker>();
        _configRepo = Substitute.For<IConfigElementRepository>();

        // embedded-subtitle extraction enabled (both feature flags on)
        _configRepo.GetValue<bool>(Arg.Any<ConfigElementKey>(), Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(Option<bool>.Some(true)));
    }

    [TearDown]
    public async Task TearDown()
    {
        await _db.DisposeAsync();
        foreach (string tempFile in _tempFiles)
        {
            if (File.Exists(tempFile))
            {
                File.Delete(tempFile);
            }
        }
    }

    private ExtractEmbeddedSubtitlesHandler CreateHandler() =>
        new(
            _db.Factory,
            Substitute.For<IFileSystem>(),
            _entityLocker,
            _configRepo,
            _worker,
            NullLogger<ExtractEmbeddedSubtitlesHandler>.Instance);

    // FFmpegPathMustExist reads ConfigElements + static File.Exists, so point ffmpeg at a real temp file
    private async Task SeedFFmpegPath()
    {
        string tempFile = Path.GetTempFileName();
        _tempFiles.Add(tempFile);
        await using TvContext context = _db.CreateContext();
        context.ConfigElements.Add(new ConfigElement { Key = ConfigElementKey.FFmpegPath.Key, Value = tempFile });
        await context.SaveChangesAsync();
    }

    private async Task<int> SeedSubtitledPlayout()
    {
        int number = ++_channelNumber;
        await using TvContext context = _db.CreateContext();
        var channel = new ErsatzTV.Core.Domain.Channel(Guid.NewGuid())
        {
            Number = number.ToString(),
            SortNumber = number,
            Name = "Subs",
            Group = string.Empty,
            Categories = string.Empty,
            StreamingMode = StreamingMode.HttpLiveStreamingSegmenter,
            SubtitleMode = ChannelSubtitleMode.Any,
            Playouts = [],
            Artwork = [],
            StreamSelector = string.Empty,
            PreferredAudioLanguageCode = string.Empty,
            PreferredAudioTitle = string.Empty,
            PreferredSubtitleLanguageCode = string.Empty,
            MusicVideoCreditsTemplate = string.Empty,
            PlayoutSource = ChannelPlayoutSource.Generated,
            PlayoutMode = ChannelPlayoutMode.Continuous,
            IsEnabled = true,
            ShowInEpg = true
        };
        var playout = new Playout { Channel = channel };
        context.Playouts.Add(playout);
        await context.SaveChangesAsync();
        return playout.Id;
    }

    [Test]
    public async Task Extract_Should_Release_All_Locks_On_Cancellation()
    {
        await SeedFFmpegPath();
        int playoutId = await SeedSubtitledPlayout();
        using var cts = new CancellationTokenSource();

        // win the lock, but cancel as we do it so the next cancellable DB call throws mid-run —
        // exercising the terminal path that used to skip the unlock loop
        _entityLocker.LockPlayout(Arg.Any<int>()).Returns(_ =>
        {
            cts.Cancel();
            return Task.FromResult(true);
        });

        ExtractEmbeddedSubtitlesHandler handler = CreateHandler();

        try
        {
            await handler.Handle(new ExtractEmbeddedSubtitles(Option<int>.None), cts.Token);
        }
        catch (OperationCanceledException)
        {
            // expected: the post-extract ReleaseMemory write observes the cancelled token AFTER
            // ExtractAll's finally has already released the locks (which is what we assert below)
        }

        await _entityLocker.Received(1).UnlockPlayout(playoutId);
    }

    [Test]
    public async Task Extract_Should_Release_Only_Locks_It_Acquired()
    {
        await SeedFFmpegPath();
        int ownedPlayoutId = await SeedSubtitledPlayout();
        int otherOwnersPlayoutId = await SeedSubtitledPlayout();

        // we win one lock; the other is already held by someone else (LockPlayout -> false)
        _entityLocker.LockPlayout(ownedPlayoutId).Returns(true);
        _entityLocker.LockPlayout(otherOwnersPlayoutId).Returns(false);

        ExtractEmbeddedSubtitlesHandler handler = CreateHandler();

        // no playout items seeded -> nothing to extract -> normal completion through the finally
        await handler.Handle(new ExtractEmbeddedSubtitles(Option<int>.None), CancellationToken.None);

        await _entityLocker.Received(1).UnlockPlayout(ownedPlayoutId);
        // must NOT release the lock we never acquired (no cross-release)
        await _entityLocker.DidNotReceive().UnlockPlayout(otherOwnersPlayoutId);
    }
}
