using Microsoft.Extensions.DependencyInjection;
using Moq;
using System;
using AsbCloudInfrastructure.Background;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace AsbCloudWebApi.Tests.ServicesTests
{
    public class BackgroundWorkerTest
    {
        private readonly Mock<IServiceProvider> mockServiceProvider;
        private readonly Mock<IServiceScopeFactory> mockServiceScopeFactory;
        private readonly Func<string, IServiceProvider, CancellationToken, Task> someAction = (string id, IServiceProvider scope, CancellationToken token) => Task.CompletedTask;

        public BackgroundWorkerTest()
        {
            var mockServiceScope = new Mock<IServiceScope>();
            mockServiceScopeFactory = new Mock<IServiceScopeFactory>();
            mockServiceProvider = new Mock<IServiceProvider>();

            mockServiceScope.SetReturnsDefault(mockServiceProvider.Object);
            mockServiceProvider.SetReturnsDefault(mockServiceScopeFactory.Object);
            mockServiceProvider.Setup(s => s.GetService(It.IsAny<Type>()))
                .Returns(mockServiceScopeFactory.Object);
            mockServiceScopeFactory.SetReturnsDefault(mockServiceScope.Object);
        }

        [Fact]
        public void Contains_returns_true()
        {
            mockServiceScopeFactory.Invocations.Clear();

            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            const string work1Id = "long name 1";
            const string work2Id = "long name 2";

            var work1 = new WorkBase(work1Id, someAction);            
            var work2 = new WorkPeriodic(work2Id, someAction, TimeSpan.Zero);

            BackgroundWorker.Push(work1);
            BackgroundWorker.Push(work2);

            Assert.True(BackgroundWorker.Contains(work1Id));
            Assert.True(BackgroundWorker.Contains(work2Id));
            Assert.False(BackgroundWorker.Contains(work2Id + work1Id));
            Assert.False(BackgroundWorker.Contains(string.Empty));
        }

        [Fact]
        public async Task Push_makes_new_scope_after_start()
        {
            mockServiceScopeFactory.Invocations.Clear();

            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            var work = new WorkBase("", someAction);
            BackgroundWorker.Push(work);
            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(10);

            mockServiceScopeFactory.Verify(f => f.CreateScope());
        }

        [Fact]
        public async Task Makes_primary_work_done()
        {
            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            var workDone = false;
            var work = new WorkBase("", (_, _, _) =>
            {
                workDone = true;
                return Task.CompletedTask;
            });
            BackgroundWorker.Push(work);
            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(10);

            Assert.True(workDone);
        }

        [Fact]
        public async Task Sets_ExecutionTime_after_work_done()
        {
            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            var work = new WorkBase("", someAction);
            BackgroundWorker.Push(work);
            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(10);

            Assert.True(work.ExecutionTime > TimeSpan.Zero);
        }

        [Fact]
        public async Task Makes_periodic_work_done()
        {
            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            var workDone = false;
            var work = new WorkPeriodic("", (_, _, _) =>
            {
                workDone = true;
                return Task.CompletedTask;
            },
            TimeSpan.FromMilliseconds(10));
            BackgroundWorker.Push(work);
            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(20);

            Assert.True(workDone);
        }

        [Fact]
        public async Task Does_not_start_periodic_work()
        {
            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            var workDone = false;
            var work = new WorkPeriodic("", (_, _, _) =>
            {
                workDone = true;
                return Task.CompletedTask;
            },
            TimeSpan.FromSeconds(30))
            {
                LastStart = DateTime.Now
            };
            BackgroundWorker.Push(work);

            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(20);

            Assert.False(workDone);
        }

        [Fact]
        public async Task Follows_work_priority()
        {
            var order = 0;
            var work1Order = -1;
            var work2Order = -1;

            var work1 = new WorkPeriodic("1", (_, _, _) =>
            {
                work1Order = order++;
                return Task.CompletedTask;
            },
            TimeSpan.FromMilliseconds(1)
            );

            var work2 = new WorkBase("2", (_, _, _) =>
            {
                work2Order = order++;
                return Task.CompletedTask;
            });

            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            BackgroundWorker.Push(work2);
            BackgroundWorker.Push(work1);

            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(2_100);

            Assert.True(work2Order < work1Order);
        }

        [Fact]
        public async Task Runs_second_after_delete_first()
        {
            var workDone = false;

            var work1 = new WorkBase("1", someAction);
            var work2 = new WorkPeriodic("2", (_, _, _) =>
            {
                workDone = true;
                return Task.CompletedTask;
            }, TimeSpan.FromMilliseconds(1));

            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            BackgroundWorker.Push(work1);
            BackgroundWorker.Push(work2);
            BackgroundWorker.Delete("1");

            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(10);

            Assert.True(workDone);
        }

        [Fact]
        public async Task Aborts_long_work()
        {
            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            var workCanceled = false;
            var work = new WorkBase("", async (_, _, token) => await Task.Delay(1000000, token))
            {
                Timeout = TimeSpan.FromMilliseconds(1),
                OnErrorAsync = async (id, ex, token) =>
                {
                    workCanceled = ex is System.TimeoutException;
                    await Task.CompletedTask;
                }
            };

            BackgroundWorker.Push(work);
            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(20 * 4);

            Assert.True(workCanceled);
        }

        [Fact]
        public async Task Execution_continues_after_work_exception()
        {
            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            var work2done = false;
            var work1 = new WorkBase("1", (_, _, _) => throw new Exception());
            var work2 = new WorkBase("2", (_, _, _) =>
            {
                work2done = true;
                return Task.CompletedTask;
            });

            BackgroundWorker.Push(work1);
            BackgroundWorker.Push(work2);

            await BackgroundWorker.StartAsync(CancellationToken.None);
            await Task.Delay(2_100);

            Assert.True(work2done);
        }

        [Fact]
        public void Push_not_unique_id_should_throw()
        {
            var work1 = new WorkPeriodic("1", someAction, TimeSpan.FromSeconds(30));
            var work2 = new WorkBase("1", someAction);

            var BackgroundWorker = new BackgroundWorker(mockServiceProvider.Object);
            BackgroundWorker.Push(work1);

            Assert.Throws<ArgumentException>(
                () => BackgroundWorker.Push(work2));
        }
    }
}