using System.Reflection;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Emby;
using ErsatzTV.Application.Jellyfin;
using ErsatzTV.Application.Plex;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Infrastructure.Locking;
using ErsatzTV.Services;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Services;

/// <summary>
///     Regression net for the #250 cross-release bug on the per-provider collections lock (#235):
///     a scheduler-enqueued collection scan running with <c>Unlock: false</c> must NOT release a
///     collections lock owned by another party (an in-flight API scan), while <c>Unlock: true</c>
///     (an API request or the last message of a scheduler batch) must release on completion.
/// </summary>
[TestFixture]
public class ScannerServiceCollectionLockTests
{
    [Test]
    public async Task Plex_UnlockFalse_DoesNotReleaseHeldLock()
    {
        (ScannerService service, EntityLocker locker) = BuildService(
            new SynchronizePlexCollections(1, false, false, Unlock: false));

        // another party (an API scan) owns the collections lock
        locker.LockPlexCollections().ShouldBeTrue();

        await RunToCompletion(service);

        locker.ArePlexCollectionsLocked().ShouldBeTrue();
    }

    [Test]
    public async Task Plex_UnlockTrue_ReleasesHeldLock()
    {
        (ScannerService service, EntityLocker locker) = BuildService(
            new SynchronizePlexCollections(1, false, false, Unlock: true));

        locker.LockPlexCollections().ShouldBeTrue();

        await RunToCompletion(service);

        locker.ArePlexCollectionsLocked().ShouldBeFalse();
    }

    [Test]
    public async Task Jellyfin_UnlockFalse_DoesNotReleaseHeldLock()
    {
        (ScannerService service, EntityLocker locker) = BuildService(
            new SynchronizeJellyfinCollections(1, false, false, Unlock: false));

        locker.LockJellyfinCollections().ShouldBeTrue();

        await RunToCompletion(service);

        locker.AreJellyfinCollectionsLocked().ShouldBeTrue();
    }

    [Test]
    public async Task Jellyfin_UnlockTrue_ReleasesHeldLock()
    {
        (ScannerService service, EntityLocker locker) = BuildService(
            new SynchronizeJellyfinCollections(1, false, false, Unlock: true));

        locker.LockJellyfinCollections().ShouldBeTrue();

        await RunToCompletion(service);

        locker.AreJellyfinCollectionsLocked().ShouldBeFalse();
    }

    [Test]
    public async Task Emby_UnlockFalse_DoesNotReleaseHeldLock()
    {
        (ScannerService service, EntityLocker locker) = BuildService(
            new SynchronizeEmbyCollections(1, false, false, Unlock: false));

        locker.LockEmbyCollections().ShouldBeTrue();

        await RunToCompletion(service);

        locker.AreEmbyCollectionsLocked().ShouldBeTrue();
    }

    [Test]
    public async Task Emby_UnlockTrue_ReleasesHeldLock()
    {
        (ScannerService service, EntityLocker locker) = BuildService(
            new SynchronizeEmbyCollections(1, false, false, Unlock: true));

        locker.LockEmbyCollections().ShouldBeTrue();

        await RunToCompletion(service);

        locker.AreEmbyCollectionsLocked().ShouldBeFalse();
    }

    private static (ScannerService, EntityLocker) BuildService(IScannerBackgroundServiceRequest request)
    {
        var mediator = Substitute.For<IMediator>();
        // every collection sync request returns success; the finally is what we're exercising
        Either<BaseError, LanguageExt.Unit> ok = LanguageExt.Unit.Default;
        mediator.Send(Arg.Any<SynchronizePlexCollections>(), Arg.Any<CancellationToken>())
            .Returns(ok);
        mediator.Send(Arg.Any<SynchronizeJellyfinCollections>(), Arg.Any<CancellationToken>())
            .Returns(ok);
        mediator.Send(Arg.Any<SynchronizeEmbyCollections>(), Arg.Any<CancellationToken>())
            .Returns(ok);

        var locker = new EntityLocker(mediator, NullLogger<EntityLocker>.Instance);

        var provider = Substitute.For<IServiceProvider>();
        provider.GetService(typeof(IMediator)).Returns(mediator);
        provider.GetService(typeof(IEntityLocker)).Returns(locker);

        var scope = Substitute.For<IServiceScope>();
        scope.ServiceProvider.Returns(provider);

        var scopeFactory = Substitute.For<IServiceScopeFactory>();
        scopeFactory.CreateScope().Returns(scope);

        var channel = Channel.CreateUnbounded<IScannerBackgroundServiceRequest>();
        channel.Writer.TryWrite(request).ShouldBeTrue();
        channel.Writer.Complete();

        var startup = new SystemStartup();
        startup.DatabaseIsReady();
        startup.SearchIndexIsReady();

        var service = new ScannerService(
            channel.Reader,
            scopeFactory,
            startup,
            NullLogger<ScannerService>.Instance);

        return (service, locker);
    }

    private static async Task RunToCompletion(ScannerService service)
    {
        MethodInfo executeAsync = typeof(ScannerService)
            .GetMethod("ExecuteAsync", BindingFlags.NonPublic | BindingFlags.Instance)!;

        // the channel writer is already completed, so the read loop drains the one queued
        // request, runs its finally, then ExecuteAsync returns
        var task = (Task)executeAsync.Invoke(service, [CancellationToken.None])!;
        await task.WaitAsync(TimeSpan.FromSeconds(10));
    }
}
