﻿using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using Xunit;

namespace OnionSeed.Data
{
	[SuppressMessage("AsyncUsage.CSharp.Naming", "UseAsyncSuffix:Use Async suffix", Justification = "Test methods don't need to end in 'Async'.")]
	public class AsyncRepositoryExceptionHandlerTests
	{
		private readonly Mock<IAsyncRepository<FakeEntity<int>, int>> _mockInner = new Mock<IAsyncRepository<FakeEntity<int>, int>>(MockBehavior.Strict);

		[Theory]
		[InlineData(false, false)]
		[InlineData(false, true)]
		[InlineData(true, false)]
		public void Constructor_ShouldValidateParameters(bool includeInner, bool includeHandler)
		{
			// Arrange
			var inner = includeInner ? _mockInner.Object : null;
			var handler = includeHandler ? (Exception ex) => { } : (Action<Exception>)null;

			// Act
			Action action = () => new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(inner, handler);

			// Arrange
			action.Should().Throw<ArgumentNullException>();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task AddAsync_ShouldCallInner()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.AddAsync(entity))
				.Returns(CompletedTask());

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.AddAsync(entity).ConfigureAwait(false);

			// Assert
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task AddAsync_ShouldInvokeHandle_WhenInnerThrowsException()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.AddAsync(entity))
				.Returns(ExceptionTask(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.AddAsync(entity).ConfigureAwait(false);

			// Assert
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task AddOrUpdateAsync_ShouldCallInner()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.AddOrUpdateAsync(entity))
				.Returns(CompletedTask());

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.AddOrUpdateAsync(entity).ConfigureAwait(false);

			// Assert
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task AddOrUpdateAsync_ShouldInvokeHandle_WhenInnerThrowsException()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.AddOrUpdateAsync(entity))
				.Returns(ExceptionTask(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.AddOrUpdateAsync(entity).ConfigureAwait(false);

			// Assert
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task UpdateAsync_ShouldCallInner()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.UpdateAsync(entity))
				.Returns(CompletedTask());

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.UpdateAsync(entity).ConfigureAwait(false);

			// Assert
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task UpdateAsync_ShouldInvokeHandle_WhenInnerThrowsException()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.UpdateAsync(entity))
				.Returns(ExceptionTask(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.UpdateAsync(entity).ConfigureAwait(false);

			// Assert
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task RemoveAsync_ShouldCallInner_WhenEntityIsGiven()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.RemoveAsync(entity))
				.Returns(CompletedTask());

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.RemoveAsync(entity).ConfigureAwait(false);

			// Assert
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task RemoveAsync_ShouldInvokeHandle_WhenEntityIsGiven_AndInnerThrowsException()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.RemoveAsync(entity))
				.Returns(ExceptionTask(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.RemoveAsync(entity).ConfigureAwait(false);

			// Assert
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task RemoveAsync_ShouldCallInner_WhenIdIsGiven()
		{
			// Arrange
			const int id = 47;
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.RemoveAsync(id))
				.Returns(CompletedTask());

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.RemoveAsync(id).ConfigureAwait(false);

			// Assert
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task RemoveAsync_ShouldInvokeHandle_WhenIdIsGiven_AndInnerThrowsException()
		{
			// Arrange
			const int id = 47;
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.RemoveAsync(id))
				.Returns(ExceptionTask(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			await subject.RemoveAsync(id).ConfigureAwait(false);

			// Assert
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task TryAddAsync_ShouldCallInner_AndReturnResult()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.TryAddAsync(entity))
				.ReturnsAsync(true);

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			var result = await subject.TryAddAsync(entity).ConfigureAwait(false);

			// Assert
			result.Should().BeTrue();
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task TryAddAsync_ShouldInvokeHandle_WhenInnerThrowsException()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.TryAddAsync(entity))
				.Returns(ExceptionTask<bool>(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			var result = await subject.TryAddAsync(entity).ConfigureAwait(false);

			// Assert
			result.Should().BeFalse();
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task TryUpdateAsync_ShouldCallInner_AndReturnResult()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.TryUpdateAsync(entity))
				.ReturnsAsync(true);

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			var result = await subject.TryUpdateAsync(entity).ConfigureAwait(false);

			// Assert
			result.Should().BeTrue();
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task TryUpdateAsync_ShouldInvokeHandle_WhenInnerThrowsException()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.TryUpdateAsync(entity))
				.Returns(ExceptionTask<bool>(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			var result = await subject.TryUpdateAsync(entity).ConfigureAwait(false);

			// Assert
			result.Should().BeFalse();
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task TryRemoveAsync_ShouldCallInner_AndReturnResult_WhenEntityIsGiven()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.TryRemoveAsync(entity))
				.ReturnsAsync(true);

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			var result = await subject.TryRemoveAsync(entity).ConfigureAwait(false);

			// Assert
			result.Should().BeTrue();
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task TryRemoveAsync_ShouldInvokeHandle_WhenEntityIsGiven_AndInnerThrowsException()
		{
			// Arrange
			var entity = new FakeEntity<int>() { Id = 47 };
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.TryRemoveAsync(entity))
				.Returns(ExceptionTask<bool>(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			var result = await subject.TryRemoveAsync(entity).ConfigureAwait(false);

			// Assert
			result.Should().BeFalse();
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task TryRemoveAsync_ShouldCallInner_AndReturnResult_WhenIdIsGiven()
		{
			// Arrange
			const int id = 47;
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.TryRemoveAsync(id))
				.ReturnsAsync(true);

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			var result = await subject.TryRemoveAsync(id).ConfigureAwait(false);

			// Assert
			result.Should().BeTrue();
			exception.Should().BeNull();

			_mockInner.VerifyAll();
		}

		[Fact]
		public async Task TryRemoveAsync_ShouldInvokeHandle_WhenIdIsGiven_AndInnerThrowsException()
		{
			// Arrange
			const int id = 47;
			InvalidOperationException exception = null;

			_mockInner
				.Setup(i => i.TryRemoveAsync(id))
				.Returns(ExceptionTask<bool>(new InvalidOperationException()));

			var subject = new AsyncRepositoryExceptionHandler<FakeEntity<int>, int, InvalidOperationException>(_mockInner.Object, ex => exception = ex);

			// Act
			var result = await subject.TryRemoveAsync(id).ConfigureAwait(false);

			// Assert
			result.Should().BeFalse();
			exception.Should().NotBeNull();

			_mockInner.VerifyAll();
		}

		private static Task ExceptionTask(Exception ex)
		{
#if STATIC_TASKS
			return TaskHelpers.FromException(ex);
#else
			return Task.FromException(ex);
#endif
		}

		private static Task<T> ExceptionTask<T>(Exception ex)
		{
#if STATIC_TASKS
			return TaskHelpers.FromException<T>(ex);
#else
			return Task.FromException<T>(ex);
#endif
		}

		private static Task CompletedTask()
		{
#if STATIC_TASKS
			return TaskHelpers.CompletedTask;
#else
			return Task.CompletedTask;
#endif
		}
	}
}
