// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Filters
{
    public class MiddlewareFilterBuilderTest
    {
        [Fact]
        public void GetPipeline_CallsInto_Configure()
        {
            // Arrange
            var services = new ServiceCollection();
            var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
            var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider())
            {
                ApplicationBuilder = appBuilder,
            };

            var configureCount = 0;
            Pipeline1.ConfigurePipeline = _ =>
            {
                configureCount++;
            };

            // Act
            var pipeline = pipelineBuilderService.GetPipeline(typeof(Pipeline1));

            // Assert
            Assert.NotNull(pipeline);
            Assert.Equal(1, configureCount);
        }

        [Fact]
        public void GetPipeline_CallsIntoConfigure_OnlyOnce_ForTheSamePipelineType()
        {
            // Arrange
            var services = new ServiceCollection();
            var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
            var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider())
            {
                ApplicationBuilder = appBuilder,
            };

            var configureCount = 0;
            Pipeline1.ConfigurePipeline = _ =>
            {
                configureCount++;
            };

            // Act
            var pipeline1 = pipelineBuilderService.GetPipeline(typeof(Pipeline1));

            // Assert
            Assert.NotNull(pipeline1);
            Assert.Equal(1, configureCount);

            // Act
            var pipeline2 = pipelineBuilderService.GetPipeline(typeof(Pipeline1));

            // Assert
            Assert.NotNull(pipeline2);
            Assert.Same(pipeline1, pipeline2);
            Assert.Equal(1, configureCount);
        }

        [Fact]
        public async Task EndMiddleware_ThrowsException_WhenMiddleFeature_NotAvailable()
        {
            // Arrange
            var services = new ServiceCollection();
            var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
            var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider())
            {
                ApplicationBuilder = appBuilder,
            };

            var httpContext = new DefaultHttpContext();
            Pipeline1.ConfigurePipeline = ab =>
            {
                ab.Use((ctx, next) =>
                {
                    return next(ctx);
                });
            };

            // Act
            var pipeline = pipelineBuilderService.GetPipeline(typeof(Pipeline1));

            // Assert
            Assert.NotNull(pipeline);
            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline(httpContext));
            Assert.Equal("Could not find 'IMiddlewareFilterFeature' in the feature list.", exception.Message);
        }

        [Fact]
        public async Task EndMiddleware_DoesNotThrow_IfExceptionHandled()
        {
            // Arrange
            var services = new ServiceCollection();
            var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
            var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider())
            {
                ApplicationBuilder = appBuilder,
            };

            Pipeline1.ConfigurePipeline = ab =>
            {
                ab.Use((ctx, next) =>
                {
                    return next(ctx);
                });
            };

            var middlewareFilterFeature = new MiddlewareFilterFeature
            {
                ResourceExecutionDelegate = () =>
                {
                    var actionContext = new ActionContext(
                        new DefaultHttpContext(),
                        new RouteData(),
                        new ActionDescriptor(),
                        new ModelStateDictionary());
                    var context = new ResourceExecutedContext(actionContext, new List<IFilterMetadata>())
                    {
                        Exception = new InvalidOperationException("Error!!!"),
                        ExceptionHandled = true,
                    };

                    return Task.FromResult(context);
                },
            };

            var features = new FeatureCollection();
            features.Set<IMiddlewareFilterFeature>(middlewareFilterFeature);
            var httpContext = new DefaultHttpContext(features);

            // Act
            var pipeline = pipelineBuilderService.GetPipeline(typeof(Pipeline1));

            // Assert
            Assert.NotNull(pipeline);

            // Does not throw.
            await pipeline(httpContext);
        }

        [Fact]
        public async Task EndMiddleware_PropagatesBackException_ToEarlierMiddleware()
        {
            // Arrange
            var services = new ServiceCollection();
            var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
            var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider())
            {
                ApplicationBuilder = appBuilder,
            };

            Pipeline1.ConfigurePipeline = ab =>
            {
                ab.Use((ctx, next) =>
                {
                    return next(ctx);
                });
            };

            var middlewareFilterFeature = new MiddlewareFilterFeature
            {
                ResourceExecutionDelegate = () =>
                {
                    Exception thrownException;
                    try
                    {
                        // Create a small stack trace.
                        throw new InvalidOperationException("Error!!!");
                    }
                    catch (Exception ex)
                    {
                        thrownException = ex;
                    }

                    var actionContext = new ActionContext(
                        new DefaultHttpContext(),
                        new RouteData(),
                        new ActionDescriptor(),
                        new ModelStateDictionary());
                    var context = new ResourceExecutedContext(actionContext, new List<IFilterMetadata>())
                    {
                        Exception = thrownException,
                    };

                    return Task.FromResult(context);
                },
            };

            var features = new FeatureCollection();
            features.Set<IMiddlewareFilterFeature>(middlewareFilterFeature);
            var httpContext = new DefaultHttpContext(features);

            // Act
            var pipeline = pipelineBuilderService.GetPipeline(typeof(Pipeline1));

            // Assert
            Assert.NotNull(pipeline);

            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline(httpContext));
            Assert.Null(exception.InnerException);
            Assert.Equal("Error!!!", exception.Message);

            var stack = exception.StackTrace;
            Assert.Contains(typeof(MiddlewareFilterBuilder).FullName, stack);
            Assert.DoesNotContain(typeof(MiddlewareFilterBuilderTest).FullName, stack);
            Assert.DoesNotContain(nameof(EndMiddleware_PropagatesBackException_ToEarlierMiddleware), stack);
        }

        [Fact]
        public async Task EndMiddleware_PropagatesFullExceptionInfo_ToEarlierMiddleware()
        {
            // Arrange
            var services = new ServiceCollection();
            var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
            var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider())
            {
                ApplicationBuilder = appBuilder,
            };

            Pipeline1.ConfigurePipeline = ab =>
            {
                ab.Use((ctx, next) =>
                {
                    return next(ctx);
                });
            };

            var middlewareFilterFeature = new MiddlewareFilterFeature
            {
                ResourceExecutionDelegate = () =>
                {
                    ExceptionDispatchInfo exceptionInfo;
                    try
                    {
                        // Create a small stack trace.
                        throw new InvalidOperationException("Error!!!");
                    }
                    catch (Exception ex)
                    {
                        exceptionInfo = ExceptionDispatchInfo.Capture(ex);
                    }

                    var actionContext = new ActionContext(
                        new DefaultHttpContext(),
                        new RouteData(),
                        new ActionDescriptor(),
                        new ModelStateDictionary());
                    var context = new ResourceExecutedContext(actionContext, new List<IFilterMetadata>())
                    {
                        ExceptionDispatchInfo = exceptionInfo,
                    };

                    return Task.FromResult(context);
                },
            };

            var features = new FeatureCollection();
            features.Set<IMiddlewareFilterFeature>(middlewareFilterFeature);
            var httpContext = new DefaultHttpContext(features);

            // Act
            var pipeline = pipelineBuilderService.GetPipeline(typeof(Pipeline1));

            // Assert
            Assert.NotNull(pipeline);

            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline(httpContext));
            Assert.Null(exception.InnerException);
            Assert.Equal("Error!!!", exception.Message);

            var stack = exception.StackTrace;
            Assert.Contains(typeof(MiddlewareFilterBuilder).FullName, stack);
            Assert.Contains(typeof(MiddlewareFilterBuilderTest).FullName, stack);
            Assert.Contains(nameof(EndMiddleware_PropagatesFullExceptionInfo_ToEarlierMiddleware), stack);
        }

        private class Pipeline1
        {
            public static Action<IApplicationBuilder> ConfigurePipeline { get; set; }

            public void Configure(IApplicationBuilder appBuilder)
            {
                ConfigurePipeline(appBuilder);
            }
        }
    }
}
