// 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.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
    public class PageInvokerProviderTest
    {
        [Fact]
        public void OnProvidersExecuting_WithEmptyModel_PopulatesCacheEntry()
        {
            // Arrange
            var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor
            {
                RelativePath = "/Path1",
                FilterDescriptors = new FilterDescriptor[0],
            });

            Func<PageContext, ViewContext, object> factory = (a, b) => null;
            Func<PageContext, ViewContext, object, ValueTask> releaser = (a, b, c) => default;

            var loader = Mock.Of<PageLoader>();

            var pageFactoryProvider = new Mock<IPageFactoryProvider>();
            pageFactoryProvider
                .Setup(f => f.CreatePageFactory(It.IsAny<CompiledPageActionDescriptor>()))
                .Returns(factory);
            pageFactoryProvider
                .Setup(f => f.CreateAsyncPageDisposer(It.IsAny<CompiledPageActionDescriptor>()))
                .Returns(releaser);

            var invokerProvider = CreateInvokerProvider(
                loader,
                pageFactoryProvider.Object);

            var context = new ActionInvokerProviderContext(new ActionContext()
            {
                ActionDescriptor = descriptor,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });

            // Act
            invokerProvider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.Result);
            var actionInvoker = Assert.IsType<PageActionInvoker>(context.Result);
            var entry = actionInvoker.CacheEntry;
            Assert.Equal(descriptor.RelativePath, entry.ActionDescriptor.RelativePath);
            Assert.Same(factory, entry.PageFactory);
            Assert.Same(releaser, entry.ReleasePage);
            Assert.Null(entry.ModelFactory);
            Assert.Null(entry.ReleaseModel);
            Assert.NotNull(entry.ViewDataFactory);
        }

        [Fact]
        public void OnProvidersExecuting_WithModel_PopulatesCacheEntry()
        {
            // Arrange
            var descriptor = CreateCompiledPageActionDescriptor(
                new PageActionDescriptor
                {
                    RelativePath = "/Path1",
                    FilterDescriptors = new FilterDescriptor[0]
                },
                pageType: typeof(PageWithModel),
                modelType: typeof(DerivedTestPageModel));

            Func<PageContext, ViewContext, object> factory = (a, b) => null;
            Func<PageContext, ViewContext, object, ValueTask> releaser = (a, b, c) => default;
            Func<PageContext, object> modelFactory = _ => null;
            Func<PageContext, object, ValueTask> modelDisposer = (_, __) => default;

            var loader = Mock.Of<PageLoader>();
            var pageFactoryProvider = new Mock<IPageFactoryProvider>();
            pageFactoryProvider
                .Setup(f => f.CreatePageFactory(It.IsAny<CompiledPageActionDescriptor>()))
                .Returns(factory);
            pageFactoryProvider
                .Setup(f => f.CreateAsyncPageDisposer(It.IsAny<CompiledPageActionDescriptor>()))
                .Returns(releaser);

            var modelFactoryProvider = new Mock<IPageModelFactoryProvider>();
            modelFactoryProvider
                .Setup(f => f.CreateModelFactory(It.IsAny<CompiledPageActionDescriptor>()))
                .Returns(modelFactory);
            modelFactoryProvider
                .Setup(f => f.CreateAsyncModelDisposer(It.IsAny<CompiledPageActionDescriptor>()))
                .Returns(modelDisposer);

            var invokerProvider = CreateInvokerProvider(
                loader,
                pageFactoryProvider.Object,
                modelFactoryProvider.Object);

            var context = new ActionInvokerProviderContext(new ActionContext()
            {
                ActionDescriptor = descriptor,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });

            // Act
            invokerProvider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.Result);

            var actionInvoker = Assert.IsType<PageActionInvoker>(context.Result);

            var entry = actionInvoker.CacheEntry;
            var compiledPageActionDescriptor = Assert.IsType<CompiledPageActionDescriptor>(entry.ActionDescriptor);
            Assert.Equal(descriptor.RelativePath, compiledPageActionDescriptor.RelativePath);
            Assert.Same(factory, entry.PageFactory);
            Assert.Same(releaser, entry.ReleasePage);
            Assert.Same(modelFactory, entry.ModelFactory);
            Assert.Same(modelDisposer, entry.ReleaseModel);
            Assert.NotNull(entry.ViewDataFactory);

            var pageContext = actionInvoker.PageContext;
            Assert.Same(compiledPageActionDescriptor, pageContext.ActionDescriptor);
            Assert.Same(context.ActionContext.HttpContext, pageContext.HttpContext);
            Assert.Same(context.ActionContext.ModelState, pageContext.ModelState);
            Assert.Same(context.ActionContext.RouteData, pageContext.RouteData);
            Assert.Empty(pageContext.ValueProviderFactories);
            Assert.NotNull(Assert.IsType<ViewDataDictionary<TestPageModel>>(pageContext.ViewData));
            Assert.Empty(pageContext.ViewStartFactories);
        }

        [Fact]
        public void OnProvidersExecuting_CachesViewStartFactories()
        {
            // Arrange
            var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor
            {
                RelativePath = "/Home/Path1/File.cshtml",
                ViewEnginePath = "/Home/Path1/File.cshtml",
                FilterDescriptors = new FilterDescriptor[0],
            }, pageType: typeof(PageWithModel));

            var loader = Mock.Of<PageLoader>();
            var razorPageFactoryProvider = new Mock<IRazorPageFactoryProvider>();

            Func<IRazorPage> factory1 = () => null;
            Func<IRazorPage> factory2 = () => null;

            razorPageFactoryProvider
                .Setup(f => f.CreateFactory("/Home/Path1/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), factory1));
            razorPageFactoryProvider
                .Setup(f => f.CreateFactory("/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), factory2));

            var fileProvider = new TestFileProvider();
            fileProvider.AddFile("/Home/Path1/_ViewStart.cshtml", "content1");
            fileProvider.AddFile("/_ViewStart.cshtml", "content2");

            var invokerProvider = CreateInvokerProvider(
                loader,
                razorPageFactoryProvider: razorPageFactoryProvider.Object);

            var context = new ActionInvokerProviderContext(new ActionContext()
            {
                ActionDescriptor = descriptor,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });

            // Act
            invokerProvider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.Result);
            var actionInvoker = Assert.IsType<PageActionInvoker>(context.Result);
            var entry = actionInvoker.CacheEntry;
            Assert.Equal(new[] { factory2, factory1 }, entry.ViewStartFactories);
        }

        [Fact]
        public void OnProvidersExecuting_CachesEntries()
        {
            // Arrange
            var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor
            {
                RelativePath = "/Path1",
                FilterDescriptors = new FilterDescriptor[0],
            });

            var loader = Mock.Of<PageLoader>();

            var invokerProvider = CreateInvokerProvider(
                loader);

            var context = new ActionInvokerProviderContext(new ActionContext
            {
                ActionDescriptor = descriptor,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });

            // Act - 1
            invokerProvider.OnProvidersExecuting(context);

            // Assert - 1
            Assert.NotNull(context.Result);
            var actionInvoker = Assert.IsType<PageActionInvoker>(context.Result);
            var entry1 = actionInvoker.CacheEntry;

            // Act - 2
            context = new ActionInvokerProviderContext(new ActionContext
            {
                ActionDescriptor = descriptor,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });
            invokerProvider.OnProvidersExecuting(context);

            // Assert - 2
            Assert.NotNull(context.Result);
            actionInvoker = Assert.IsType<PageActionInvoker>(context.Result);
            var entry2 = actionInvoker.CacheEntry;
            Assert.Same(entry1, entry2);
        }

        [Fact]
        public void OnProvidersExecuting_DoesNotInvokePageLoader_WhenEndpointRoutingIsUsed()
        {
            // Arrange
            var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor
            {
                RelativePath = "/Path1",
                FilterDescriptors = new FilterDescriptor[0],
            });

            var loader = new Mock<PageLoader>();
            var invokerProvider = CreateInvokerProvider(
                loader.Object,
                mvcOptions: new MvcOptions { EnableEndpointRouting = true });

            var context = new ActionInvokerProviderContext(new ActionContext
            {
                ActionDescriptor = descriptor,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });

            // Act
            invokerProvider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.Result);
            Assert.IsType<PageActionInvoker>(context.Result);
            loader.Verify(l => l.LoadAsync(It.IsAny<PageActionDescriptor>(), It.IsAny<EndpointMetadataCollection>()), Times.Never());
        }

        [Fact]
        public void OnProvidersExecuting_InvokesPageLoader_WithoutEndpointRouting()
        {
            // Arrange
            var descriptor = new PageActionDescriptor
            {
                RelativePath = "/Path1",
                FilterDescriptors = new FilterDescriptor[0],
            };

            var loader = new Mock<PageLoader>();
            loader.Setup(l => l.LoadAsync(descriptor, EndpointMetadataCollection.Empty))
                .ReturnsAsync(CreateCompiledPageActionDescriptor(descriptor));

            var invokerProvider = CreateInvokerProvider(
                loader.Object,
                mvcOptions: new MvcOptions { EnableEndpointRouting = false });

            var context = new ActionInvokerProviderContext(new ActionContext
            {
                ActionDescriptor = descriptor,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });

            // Act
            invokerProvider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.Result);
            Assert.IsType<PageActionInvoker>(context.Result);
            loader.Verify(l => l.LoadAsync(It.IsAny<PageActionDescriptor>(), It.IsAny<EndpointMetadataCollection>()), Times.Once());
        }

        [Fact]
        public void CacheUpdatesWhenDescriptorChanges()
        {
            // Arrange
            var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor
            {
                RelativePath = "/Path1",
                FilterDescriptors = new FilterDescriptor[0],
            });

            var descriptor2 = CreateCompiledPageActionDescriptor(new PageActionDescriptor
            {
                RelativePath = "/Path1",
                FilterDescriptors = new FilterDescriptor[0],
            });

            var loader = Mock.Of<PageLoader>();

            var invokerProvider = CreateInvokerProvider(
                 loader);

            var context1 = new ActionInvokerProviderContext(new ActionContext()
            {
                ActionDescriptor = descriptor,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });

            // Act - 1
            invokerProvider.OnProvidersExecuting(context1);

            // Assert - 1
            Assert.NotNull(context1.Result);
            var actionInvoker = Assert.IsType<PageActionInvoker>(context1.Result);
            var entry1 = actionInvoker.CacheEntry;

            // Act - 2

            var context2 = new ActionInvokerProviderContext(new ActionContext()
            {
                ActionDescriptor = descriptor2,
                HttpContext = new DefaultHttpContext(),
                RouteData = new RouteData(),
            });

            invokerProvider.OnProvidersExecuting(context2);

            // Assert
            Assert.NotNull(context2.Result);
            actionInvoker = Assert.IsType<PageActionInvoker>(context2.Result);
            var entry2 = actionInvoker.CacheEntry;
            Assert.NotSame(entry1, entry2);
        }

        [Fact]
        public void GetViewStartFactories_FindsFullHierarchy()
        {

            // Arrange
            var descriptor = new PageActionDescriptor()
            {
                RelativePath = "/Pages/Level1/Level2/Index.cshtml",
                FilterDescriptors = new FilterDescriptor[0],
                ViewEnginePath = "/Pages/Level1/Level2/Index.cshtml"
            };

            var compiledPageDescriptor = new CompiledPageActionDescriptor(descriptor)
            {
                PageTypeInfo = typeof(object).GetTypeInfo(),
            };

            var loader = new Mock<PageLoader>();
            loader
                .Setup(l => l.LoadAsync(It.IsAny<PageActionDescriptor>(), It.IsAny<EndpointMetadataCollection>()))
                .ReturnsAsync(compiledPageDescriptor);

            var mock = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict);
            mock
                .Setup(p => p.CreateFactory("/Pages/Level1/Level2/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
                .Verifiable();
            mock
                .Setup(p => p.CreateFactory("/Pages/Level1/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
                .Verifiable();
            mock
                .Setup(p => p.CreateFactory("/Pages/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
                .Verifiable();
            mock
                .Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
                .Verifiable();

            var razorPageFactoryProvider = mock.Object;

            var invokerProvider = CreateInvokerProvider(
                loader.Object,
                razorPageFactoryProvider: razorPageFactoryProvider);

            // Act
            var factories = invokerProvider.Cache.GetViewStartFactories(compiledPageDescriptor);

            // Assert
            mock.Verify();
        }

        [Fact]
        public void GetViewStartFactories_ReturnsFactoriesForFilesThatDoNotExistInProject()
        {
            // The factory provider might have access to _ViewStarts for files that do not exist on disk \ RazorProject.
            // This test verifies that we query the factory provider correctly.
            // Arrange
            var descriptor = new PageActionDescriptor()
            {
                RelativePath = "/Views/Deeper/Index.cshtml",
                FilterDescriptors = new FilterDescriptor[0],
                ViewEnginePath = "/Views/Deeper/Index.cshtml"
            };

            var loader = new Mock<PageLoader>();
            loader
                .Setup(l => l.LoadAsync(It.IsAny<PageActionDescriptor>(), It.IsAny<EndpointMetadataCollection>()))
                .ReturnsAsync(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel)));

            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(f => f.CreateFactory("/Views/Deeper/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null));
            pageFactory
                .Setup(f => f.CreateFactory("/Views/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), razorPageFactory: null));
            pageFactory
                .Setup(f => f.CreateFactory("/_ViewStart.cshtml"))
                .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null));

            // No files
            var fileProvider = new TestFileProvider();

            var invokerProvider = CreateInvokerProvider(
                loader.Object,
                pageProvider: null,
                modelProvider: null,
                razorPageFactoryProvider: pageFactory.Object);

            var compiledDescriptor = CreateCompiledPageActionDescriptor(descriptor);

            // Act
            var factories = invokerProvider.Cache.GetViewStartFactories(compiledDescriptor).ToList();

            // Assert
            Assert.Equal(2, factories.Count);
        }

        private static CompiledPageActionDescriptor CreateCompiledPageActionDescriptor(
            PageActionDescriptor descriptor,
            Type pageType = null,
            Type modelType = null)
        {
            pageType = pageType ?? typeof(object);
            var pageTypeInfo = pageType.GetTypeInfo();

            var modelTypeInfo = modelType?.GetTypeInfo();
            TypeInfo declaredModelTypeInfo = null;
            if (pageType != null)
            {
                declaredModelTypeInfo = pageTypeInfo.GetProperty("Model")?.PropertyType.GetTypeInfo();
                if (modelTypeInfo == null)
                {
                    modelTypeInfo = declaredModelTypeInfo;
                }
            }

            return new CompiledPageActionDescriptor(descriptor)
            {
                HandlerTypeInfo = modelTypeInfo ?? pageTypeInfo,
                DeclaredModelTypeInfo = declaredModelTypeInfo ?? pageTypeInfo,
                ModelTypeInfo = modelTypeInfo ?? pageTypeInfo,
                PageTypeInfo = pageTypeInfo,
                FilterDescriptors = Array.Empty<FilterDescriptor>(),
            };
        }

        private static PageActionInvokerProvider CreateInvokerProvider(
            PageLoader loader,
            IPageFactoryProvider pageProvider = null,
            IPageModelFactoryProvider modelProvider = null,
            IRazorPageFactoryProvider razorPageFactoryProvider = null,
            MvcOptions mvcOptions = null)
        {
            var tempDataFactory = new Mock<ITempDataDictionaryFactory>();
            tempDataFactory
                .Setup(t => t.GetTempData(It.IsAny<HttpContext>()))
                .Returns((HttpContext context) => new TempDataDictionary(context, Mock.Of<ITempDataProvider>()));

            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
            var modelBinderFactory = TestModelBinderFactory.CreateDefault();
            mvcOptions = mvcOptions ?? new MvcOptions();

            var parameterBinder = new ParameterBinder(
                modelMetadataProvider,
                TestModelBinderFactory.CreateDefault(),
                Mock.Of<IObjectModelValidator>(),
                Options.Create(mvcOptions),
                NullLoggerFactory.Instance);

            var cache = new PageActionInvokerCache(
                pageProvider ?? Mock.Of<IPageFactoryProvider>(),
                modelProvider ?? Mock.Of<IPageModelFactoryProvider>(),
                razorPageFactoryProvider ?? Mock.Of<IRazorPageFactoryProvider>(),
                new IFilterProvider[0],
                parameterBinder,
                modelMetadataProvider,
                modelBinderFactory,
                Options.Create(mvcOptions));

            return new PageActionInvokerProvider(
                loader,
                cache,
                modelMetadataProvider,
                tempDataFactory.Object,
                Options.Create(mvcOptions),
                Options.Create(new MvcViewOptions()),
                Mock.Of<IPageHandlerMethodSelector>(),
                new DiagnosticListener("Microsoft.AspNetCore"),
                NullLoggerFactory.Instance,
                new ActionResultTypeMapper());
        }

        private class PageWithModel
        {
            public TestPageModel Model { get; set; }
        }

        private class TestPageModel
        {
            public void OnGet()
            {
            }
        }

        private class DerivedTestPageModel : TestPageModel
        {
        }
    }
}
