using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudInfrastructure.Background;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;

namespace AsbCloudInfrastructure.Tests.Background;

public class WorkTest
{
    private readonly IServiceProvider serviceProviderMock = Substitute.For<IServiceProvider, ISupportRequiredService>();
    private readonly IServiceScope serviceScopeMock = Substitute.For<IServiceScope>();
    private readonly IServiceScopeFactory serviceScopeFactoryMock = Substitute.For<IServiceScopeFactory>();

    public WorkTest()
    {
        serviceScopeFactoryMock.CreateScope().Returns(serviceScopeMock);
        ((ISupportRequiredService)serviceProviderMock).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactoryMock);
    }

    [Fact, MethodImpl(MethodImplOptions.NoOptimization)]
    public async Task Start_ShouldReturn_Success()
    {
        //arrange
        Task workAction(string id, IServiceProvider services, Action<string, double?> callback, CancellationToken token)
            => Task.CompletedTask;

        var work = Work.CreateByDelegate("", workAction);

        //act
        var begin = DateTime.Now;
        await work.Start(serviceProviderMock, CancellationToken.None);
        var done = DateTime.Now;
        var executionTime = done - begin;

        //assert
        Assert.Equal(1, work.CountComplete);
        Assert.Equal(1, work.CountStart);
        Assert.Equal(0, work.CountErrors);
        Assert.Null(work.CurrentState);
        Assert.Null(work.LastError);

        var lastState = work.LastComplete;
        Assert.NotNull(lastState);
        Assert.InRange(lastState.Start, begin, done - 0.5 * executionTime);
        Assert.InRange(lastState.End, done - 0.5 * executionTime, done);
        Assert.InRange(lastState.ExecutionTime, TimeSpan.Zero, executionTime);
    }

    [Fact, MethodImpl(MethodImplOptions.NoOptimization)]
    public async Task ExecutionWork_Invokes_Callback()
    {
        //arrange
        const string expectedState = "42";
        const double expectedProgress = 42d;

        var timeout = TimeSpan.FromMilliseconds(80);

        Task workAction(string id, IServiceProvider services, Action<string, double?> callback, CancellationToken token)
        {
            callback.Invoke(expectedState, expectedProgress);
            return Task.Delay(timeout);
        }

        var work = Work.CreateByDelegate("", workAction);

        //act
        var begin = DateTime.Now;
        _ = work.Start(serviceProviderMock, CancellationToken.None);
        await Task.Delay(timeout / 3);

        //assert
        Assert.Equal(0, work.CountComplete);
        Assert.Equal(1, work.CountStart);
        Assert.Equal(0, work.CountErrors);
        Assert.NotNull(work.CurrentState);
        Assert.Null(work.LastComplete);
        Assert.Null(work.LastError);

        var currentState = work.CurrentState;
        Assert.NotNull(currentState);
        Assert.InRange(currentState.Start, begin, begin + timeout);
        Assert.InRange(currentState.StateUpdate, begin, begin + timeout);
        Assert.Equal(expectedState, currentState.State);
        Assert.Equal(expectedProgress, currentState.Progress);
    }

    [Fact, MethodImpl(MethodImplOptions.NoOptimization)]
    public async Task ExecutionWork_ShouldReturn_FailsWithInfo()
    {
        //arrange
        const string expectedState = "41";
        const string expectedErrorText = "42";
        var minWorkTime = TimeSpan.FromMilliseconds(10);

        async Task workAction(string id, IServiceProvider services, Action<string, double?> callback, CancellationToken token)
        {
            await Task.Delay(minWorkTime);
            callback(expectedState, 0);
            throw new Exception(expectedErrorText);
        }

        var work = Work.CreateByDelegate("", workAction);

        //act
        var begin = DateTime.Now;
        await work.Start(serviceProviderMock, CancellationToken.None);

        //assert
        Assert.Equal(0, work.CountComplete);
        Assert.Equal(1, work.CountStart);
        Assert.Equal(1, work.CountErrors);
        Assert.Null(work.CurrentState);
        Assert.Null(work.LastComplete);

        var error = work.LastError;
        Assert.NotNull(error);
        Assert.InRange(error.Start, begin, DateTime.Now);
        Assert.InRange(error.End, begin, DateTime.Now);
        Assert.InRange(error.ExecutionTime, minWorkTime, DateTime.Now - begin);
        Assert.Contains(expectedErrorText, error.ErrorText, StringComparison.InvariantCultureIgnoreCase);
        Assert.Equal(expectedState, error.State);
    }

    [Fact]
    public async Task Stop_ShouldReturn_Success()
    {
        //arrange
        var workTime = TimeSpan.FromMilliseconds(1_000);

        Task workAction(string id, IServiceProvider services, Action<string, double?> callback, CancellationToken token)
            => Task.Delay(workTime, token);

        var work = Work.CreateByDelegate("", workAction);

        //act
        var begin = DateTime.Now;
        _ = work.Start(serviceProviderMock, CancellationToken.None);
        await Task.Delay(10);
        work.Stop();
        await Task.Delay(10);

        //assert
        Assert.Equal(0, work.CountComplete);
        Assert.Equal(1, work.CountStart);
        Assert.Equal(1, work.CountErrors);
        Assert.Null(work.LastComplete);
        Assert.NotNull(work.LastError);
    }
}