using e_suite.API.Common;
using e_suite.API.Common.exceptions;
using e_suite.Database.Core.Extensions.Exceptions;
using eSuite.API.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Newtonsoft.Json;
using NUnit.Framework;
using System.ComponentModel.DataAnnotations;

namespace eSuite.API.UnitTests.Middleware;

[TestFixture]
public class ExceptionCaptureUnitTests
{
    private Mock<RequestDelegate> _requestDelegateMock = null!;
    private readonly Mock<IExceptionLogManager> _exceptionLoggerMock = new Mock<IExceptionLogManager>()!;
    private ExceptionCapture _exceptionCaptureMiddleWare = null!;

    [SetUp]
    public void Setup()
    {
        _requestDelegateMock = new Mock<RequestDelegate>();
        _exceptionCaptureMiddleWare = new ExceptionCapture(_requestDelegateMock.Object, _exceptionLoggerMock.Object);
    }

    [Test]
    public void ExceptionMiddleWare_WhenCalled_InvokeCalledAndNoExceptionThrown()
    {
        //Arrange
        var context = new DefaultHttpContext();

        //Assert
        Assert.DoesNotThrowAsync(async () =>
        {
            //Act
            await _exceptionCaptureMiddleWare.InvokeAsync(context);
        });
        _requestDelegateMock.Verify(x => x.Invoke(context), Times.Once);
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenGuidMismatchThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws( () => new GuidMismatchException("Test error"));

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Bad request"));
        Assert.That(problem!.Detail, Is.EqualTo("Test error"));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenNotFoundExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<NotFoundException>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status404NotFound));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Not found"));
        Assert.That(problem!.Detail, Is.EqualTo("Exception of type 'e_suite.API.Common.exceptions.NotFoundException' was thrown."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenExistsExceptionExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<ExistsException>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Bad request"));
        Assert.That(problem!.Detail, Is.EqualTo("Exception of type 'e_suite.API.Common.exceptions.ExistsException' was thrown."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenInvalidOperationExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<InvalidOperationException>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Bad request"));
        Assert.That(problem!.Detail, Is.EqualTo("Operation is not valid due to the current state of the object."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenValidationExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<ValidationException>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Bad request"));
        Assert.That(problem!.Detail, Is.EqualTo("Exception of type 'System.ComponentModel.DataAnnotations.ValidationException' was thrown."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenArgumentExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<ArgumentException>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Bad request"));
        Assert.That(problem!.Detail, Is.EqualTo("Value does not fall within the expected range."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenInvalidReferenceObjectIdThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<InvalidReferenceObjectId>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Bad request"));
        Assert.That(problem!.Detail, Is.EqualTo("Exception of type 'e_suite.API.Common.exceptions.InvalidReferenceObjectId' was thrown."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenTokenInvalidExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<TokenInvalidException>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Bad request"));
        Assert.That(problem!.Detail, Is.EqualTo("Exception of type 'e_suite.API.Common.exceptions.TokenInvalidException' was thrown."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenInvalidEmailExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<InvalidEmailException>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Bad request"));
        Assert.That(problem!.Detail, Is.EqualTo("Exception of type 'e_suite.API.Common.exceptions.InvalidEmailException' was thrown."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenOperationCanceledExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<OperationCanceledException>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status400BadRequest));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Request cancelled"));
        Assert.That(problem!.Detail, Is.EqualTo("The operation was canceled."));
    }

    [Test]
    public async Task ExceptionMiddleWare_WhenUnhandledExceptionThrow_ExpectedResponseReturned()
    {
        //Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        _requestDelegateMock.Setup(x => x.Invoke(context)).Throws<Exception>();

        //Act
        await _exceptionCaptureMiddleWare.InvokeAsync(context);

        //Assert
        Assert.That(context.Response.StatusCode, Is.EqualTo(StatusCodes.Status500InternalServerError));

        context.Response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(context.Response.Body);
        var streamText = reader.ReadToEnd();
        var problem = JsonConvert.DeserializeObject<ProblemDetails>(streamText);

        Assert.That(problem, Is.Not.Null);
        Assert.That(problem!.Title, Is.EqualTo("Internal Server Error"));
        Assert.That(problem!.Detail, Is.EqualTo("Exception of type 'System.Exception' was thrown."));
    }
}