// 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.Diagnostics;
using System.Threading;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Razor
{
    public class RazorViewEngineTest
    {
        private static readonly Dictionary<string, object> _areaTestContext = new Dictionary<string, object>()
        {
            {"area", "foo"},
            {"controller", "bar"},
        };

        private static readonly Dictionary<string, object> _controllerTestContext = new Dictionary<string, object>()
        {
            {"controller", "bar"},
        };

        private static readonly Dictionary<string, object> _pageTestContext = new Dictionary<string, object>()
        {
            {"page", "/Accounts/Index"},
        };

        public static IEnumerable<object[]> AbsoluteViewPathData
        {
            get
            {
                yield return new[] { "~/foo/bar" };
                yield return new[] { "/foo/bar" };
                yield return new[] { "~/foo/bar.txt" };
                yield return new[] { "/foo/bar.txt" };
            }
        }

        [Theory]
        [InlineData(null)]
        [InlineData("")]
        public void FindView_IsMainPage_ThrowsIfViewNameIsNullOrEmpty(string viewName)
        {
            // Arrange
            var viewEngine = CreateViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act & Assert
            ExceptionAssert.ThrowsArgumentNullOrEmpty(
                () => viewEngine.FindView(context, viewName, isMainPage: true),
                "viewName");
        }

        [Theory]
        [MemberData(nameof(AbsoluteViewPathData))]
        public void FindView_IsMainPage_WithFullPath_ReturnsNotFound(string viewName)
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindView(context, viewName, isMainPage: true);

            // Assert
            Assert.False(result.Success);
        }

        [Theory]
        [MemberData(nameof(AbsoluteViewPathData))]
        public void FindView_IsMainPage_WithFullPathAndCshtmlEnding_ReturnsNotFound(string viewName)
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();
            var context = GetActionContext(_controllerTestContext);
            viewName += ".cshtml";

            // Act
            var result = viewEngine.FindView(context, viewName, isMainPage: true);

            // Assert
            Assert.False(result.Success);
        }

        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void FindView_WithRelativePath_ReturnsNotFound(bool isMainPage)
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindView(context, "View.cshtml", isMainPage);

            // Assert
            Assert.False(result.Success);
        }

        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GetView_WithViewName_ReturnsNotFound(bool isMainPage)
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();

            // Act
            var result = viewEngine.GetView("~/Home/View1.cshtml", "View2", isMainPage);

            // Assert
            Assert.False(result.Success);
        }

        [Fact]
        public void FindView_ReturnsRazorView_IfLookupWasSuccessful()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            var viewStart1 = Mock.Of<IRazorPage>();
            var viewStart2 = Mock.Of<IRazorPage>();

            pageFactory
                .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
                .Returns(GetPageFactoryResult(() => page));

            pageFactory
                .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
                .Returns(GetPageFactoryResult(() => viewStart2));

            pageFactory
                .Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
                .Returns(GetPageFactoryResult(() => viewStart1));

            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindView(context, "test-view", isMainPage: false);

            // Assert
            Assert.True(result.Success);
            var view = Assert.IsType<RazorView>(result.View);
            Assert.Same(page, view.RazorPage);
            Assert.Equal("test-view", result.ViewName);
            Assert.Empty(view.ViewStartPages);
        }

        [Fact]
        public void FindView_DoesNotExpireCachedResults_IfViewStartsExpire()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            var viewStart = Mock.Of<IRazorPage>();
            var cancellationTokenSource = new CancellationTokenSource();
            var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);

            pageFactory
                .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
                .Returns(GetPageFactoryResult(() => page));

            pageFactory
                .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
                .Returns(GetPageFactoryResult(() => viewStart, new[] { changeToken }));

            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(_controllerTestContext);

            // Act - 1
            var result1 = viewEngine.FindView(context, "test-view", isMainPage: false);

            // Assert - 1
            Assert.True(result1.Success);
            var view1 = Assert.IsType<RazorView>(result1.View);
            Assert.Same(page, view1.RazorPage);
            Assert.Equal("test-view", result1.ViewName);
            Assert.Empty(view1.ViewStartPages);

            // Act - 2
            cancellationTokenSource.Cancel();
            var result2 = viewEngine.FindView(context, "test-view", isMainPage: false);

            // Assert - 2
            Assert.True(result2.Success);
            var view2 = Assert.IsType<RazorView>(result2.View);
            Assert.Same(page, view2.RazorPage);
            pageFactory.Verify(p => p.CreateFactory("/Views/bar/test-view.cshtml"), Times.Once());
        }

        [Theory]
        [InlineData(null)]
        [InlineData("")]
        public void FindView_ThrowsIfViewNameIsNullOrEmpty(string partialViewName)
        {
            // Arrange
            var viewEngine = CreateViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act & Assert
            ExceptionAssert.ThrowsArgumentNullOrEmpty(
                () => viewEngine.FindView(context, partialViewName, isMainPage: false),
                "viewName");
        }

        [Theory]
        [MemberData(nameof(AbsoluteViewPathData))]
        public void FindViewWithFullPath_ReturnsNotFound(string partialViewName)
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindView(context, partialViewName, isMainPage: false);

            // Assert
            Assert.False(result.Success);
        }

        [Theory]
        [MemberData(nameof(AbsoluteViewPathData))]
        public void FindViewWithFullPathAndCshtmlEnding_ReturnsNotFound(string partialViewName)
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();
            var context = GetActionContext(_controllerTestContext);
            partialViewName += ".cshtml";

            // Act
            var result = viewEngine.FindView(context, partialViewName, isMainPage: false);

            // Assert
            Assert.False(result.Success);
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void FindView_FailsButSearchesCorrectLocationsWithAreas(bool isMainPage)
        {
            // Arrange
            var viewEngine = CreateViewEngine();
            var context = GetActionContext(_areaTestContext);

            // Act
            var result = viewEngine.FindView(context, "viewName", isMainPage);

            // Assert
            Assert.False(result.Success);
            Assert.Equal(new[]
            {
                "/Areas/foo/Views/bar/viewName.cshtml",
                "/Areas/foo/Views/Shared/viewName.cshtml",
                "/Views/Shared/viewName.cshtml",
            }, result.SearchedLocations);
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void FindView_FailsButSearchesCorrectLocationsWithoutAreas(bool isMainPage)
        {
            // Arrange
            var viewEngine = CreateViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindView(context, "viewName", isMainPage);

            // Assert
            Assert.False(result.Success);
            Assert.Equal(new[] {
                "/Views/bar/viewName.cshtml",
                "/Views/Shared/viewName.cshtml",
            }, result.SearchedLocations);
        }

        [Fact]
        public void FindView_IsMainPage_ReturnsRazorView_IfLookupWasSuccessful()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            var viewStart1 = Mock.Of<IRazorPage>();
            var viewStart2 = Mock.Of<IRazorPage>();

            pageFactory
                .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
                .Returns(GetPageFactoryResult(() => page));

            pageFactory
                .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
                .Returns(GetPageFactoryResult(() => viewStart2));

            pageFactory
                .Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
                .Returns(GetPageFactoryResult(() => viewStart1));

            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindView(context, "test-view", isMainPage: true);

            // Assert
            Assert.True(result.Success);
            var view = Assert.IsType<RazorView>(result.View);
            Assert.Equal("test-view", result.ViewName);
            Assert.Same(page, view.RazorPage);

            // ViewStartPages is not empty as it is in FindView_ReturnsRazorView_IfLookupWasSuccessful() despite
            // (faked) existence of the view start files in both tests.
            Assert.Equal(new[] { viewStart1, viewStart2 }, view.ViewStartPages);
        }

        [Fact]
        public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();

            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory("fake-path1/bar/test-view.rzr"))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor(
                    viewLocationFormats: new[] { "fake-path1/{1}/{0}.rzr" },
                    areaViewLocationFormats: new[] { "fake-area-path/{2}/{1}/{0}.rzr" }));
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindView(context, "test-view", isMainPage: true);

            // Assert
            pageFactory.Verify();
            var view = Assert.IsType<RazorView>(result.View);
            Assert.Same(page, view.RazorPage);
        }

        [Fact]
        public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea()
        {
            // Arrange
            var viewName = "test-view2";
            var expectedViewName = "fake-area-path/foo/bar/test-view2.rzr";
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(expectedViewName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor(
                    viewLocationFormats: new[] { "fake-path1/{1}/{0}.rzr" },
                    areaViewLocationFormats: new[] { "fake-area-path/{2}/{1}/{0}.rzr" }));
            var context = GetActionContext(_areaTestContext);

            // Act
            var result = viewEngine.FindView(context, viewName, isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.Equal(viewName, result.ViewName);
            pageFactory.Verify();
        }

        [Theory]
        [InlineData("Test-View.cshtml")]
        [InlineData("/Home/Test-View.cshtml")]
        public void GetView_DoesNotUseViewLocationFormat_WithRelativePath_IfRouteDoesNotContainArea(string viewName)
        {
            // Arrange
            var expectedViewName = "/Home/Test-View.cshtml";
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(expectedViewName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act
            var result = viewEngine.GetView("/Home/Page.cshtml", viewName, isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.Equal(viewName, result.ViewName);
            pageFactory.Verify();
        }

        [Theory]
        [InlineData("Test-View.cshtml")]
        [InlineData("/Home/Test-View.cshtml")]
        public void GetView_DoesNotUseViewLocationFormat_WithRelativePath_IfRouteContainArea(string viewName)
        {
            // Arrange
            var expectedViewName = "/Home/Test-View.cshtml";
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(expectedViewName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act
            var result = viewEngine.GetView("/Home/Page.cshtml", viewName, isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.Equal(viewName, result.ViewName);
            pageFactory.Verify();
        }

        [Theory]
        [InlineData("/Test-View.cshtml")]
        [InlineData("~/Test-View.CSHTML")]
        [InlineData("/Home/Test-View.CSHTML")]
        [InlineData("~/Home/Test-View.cshtml")]
        [InlineData("~/SHARED/TEST-VIEW.CSHTML")]
        public void GetView_UsesGivenPath_WithAppRelativePath(string viewName)
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(viewName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act
            var result = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.Equal(viewName, result.ViewName);
            pageFactory.Verify();
        }

        [Theory]
        [InlineData("Test-View.cshtml")]
        [InlineData("Test-View.CSHTML")]
        [InlineData("PATH/TEST-VIEW.CSHTML")]
        [InlineData("Path1/Path2/Test-View.cshtml")]
        public void GetView_ResolvesRelativeToCurrentPage_WithRelativePath(string viewName)
        {
            // Arrange
            var expectedViewName = $"/Home/{ viewName }";
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(expectedViewName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act
            var result = viewEngine.GetView("/Home/Page.cshtml", viewName, isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.Equal(viewName, result.ViewName);
            pageFactory.Verify();
        }

        [Theory]
        [InlineData("Test-View.cshtml")]
        [InlineData("Test-View.CSHTML")]
        [InlineData("PATH/TEST-VIEW.CSHTML")]
        [InlineData("Path1/Path2/Test-View.cshtml")]
        public void GetView_ResolvesRelativeToAppRoot_WithRelativePath_IfNoPageExecuting(string viewName)
        {
            // Arrange
            var expectedViewName = $"/{ viewName }";
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(expectedViewName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act
            var result = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.Equal(viewName, result.ViewName);
            pageFactory.Verify();
            var view = Assert.IsType<RazorView>(result.View);
            Assert.Same(page, view.RazorPage);
        }

        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void FindView_CreatesDifferentCacheEntries_ForAreaViewsAndNonAreaViews(bool isMainPage)
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var areaPage = Mock.Of<IRazorPage>();
            var nonAreaPage = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"))
                .Returns(GetPageFactoryResult(() => areaPage));
            pageFactory
                .Setup(p => p.CreateFactory("/Views/Home/Index.cshtml"))
                .Returns(GetPageFactoryResult(() => nonAreaPage));

            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act 1
            var areaContext = GetActionContext(new Dictionary<string, object>()
            {
                {"area", "Admin"},
                {"controller", "Home"},
            });
            var result1 = viewEngine.FindView(areaContext, "Index", isMainPage);

            // Assert 1
            Assert.NotNull(result1);
            pageFactory.Verify(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"), Times.Once());
            pageFactory.Verify(p => p.CreateFactory("/Views/Home/Index.cshtml"), Times.Never());
            var view1 = Assert.IsType<RazorView>(result1.View);
            Assert.Same(areaPage, view1.RazorPage);

            // Act 2
            var nonAreaContext = GetActionContext(new Dictionary<string, object>()
            {
                {"controller", "Home"},
            });
            var result2 = viewEngine.FindView(nonAreaContext, "Index", isMainPage);

            // Assert 2
            Assert.NotNull(result2);
            pageFactory.Verify(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"), Times.Once());
            pageFactory.Verify(p => p.CreateFactory("/Views/Home/Index.cshtml"), Times.Once());
            var view2 = Assert.IsType<RazorView>(result2.View);
            Assert.Same(nonAreaPage, view2.RazorPage);

            // Act 3
            // Ensure we're getting cached results.
            var result3 = viewEngine.FindView(areaContext, "Index", isMainPage);
            var result4 = viewEngine.FindView(nonAreaContext, "Index", isMainPage);

            // Assert 4
            pageFactory.Verify(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"), Times.Once());
            pageFactory.Verify(p => p.CreateFactory("/Views/Home/Index.cshtml"), Times.Once());

            var view3 = Assert.IsType<RazorView>(result3.View);
            Assert.Same(areaPage, view3.RazorPage);
            var view4 = Assert.IsType<RazorView>(result4.View);
            Assert.Same(nonAreaPage, view4.RazorPage);
        }

        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void FindView_CreatesDifferentCacheEntries_ForDifferentAreas(bool isMainPage)
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var areaPage1 = Mock.Of<IRazorPage>();
            var areaPage2 = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"))
                .Returns(GetPageFactoryResult(() => areaPage1));
            pageFactory
                .Setup(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"))
                .Returns(GetPageFactoryResult(() => areaPage2));

            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act 1
            var areaContext1 = GetActionContext(new Dictionary<string, object>()
            {
                {"area", "Marketing"},
                {"controller", "Home"},
            });
            var result1 = viewEngine.FindView(areaContext1, "Index", isMainPage);

            // Assert 1
            Assert.NotNull(result1);
            pageFactory.Verify(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"), Times.Once());
            pageFactory.Verify(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"), Times.Never());
            var view1 = Assert.IsType<RazorView>(result1.View);
            Assert.Same(areaPage1, view1.RazorPage);

            // Act 2
            var areaContext2 = GetActionContext(new Dictionary<string, object>()
            {
                {"controller", "Home"},
                {"area", "Sales"},
            });
            var result2 = viewEngine.FindView(areaContext2, "Index", isMainPage);

            // Assert 2
            Assert.NotNull(result2);
            pageFactory.Verify(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"), Times.Once());
            pageFactory.Verify(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"), Times.Once());
            var view2 = Assert.IsType<RazorView>(result2.View);
            Assert.Same(areaPage2, view2.RazorPage);

            // Act 3
            // Ensure we're getting cached results.
            var result3 = viewEngine.FindView(areaContext1, "Index", isMainPage);
            var result4 = viewEngine.FindView(areaContext2, "Index", isMainPage);

            // Assert 4
            pageFactory.Verify(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"), Times.Once());
            pageFactory.Verify(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"), Times.Once());

            var view3 = Assert.IsType<RazorView>(result3.View);
            Assert.Same(areaPage1, view3.RazorPage);
            var view4 = Assert.IsType<RazorView>(result4.View);
            Assert.Same(areaPage2, view4.RazorPage);
        }

        [Fact]
        public void FindView_UsesViewLocationExpandersToLocateViews()
        {
            // Arrange
            var routeValues = _controllerTestContext;
            var expectedSeeds = new[]
            {
                "/Views/{1}/{0}.cshtml",
                "/Views/Shared/{0}.cshtml"
            };

            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(p => p.CreateFactory("test-string/bar.cshtml"))
                .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
                .Verifiable();

            var expander1Result = new[] { "some-seed" };
            var expander1 = new Mock<IViewLocationExpander>();
            expander1
                .Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
                .Callback((ViewLocationExpanderContext c) =>
                {
                    Assert.NotNull(c.ActionContext);
                    c.Values["expander-key"] = expander1.ToString();
                })
                .Verifiable();
            expander1
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
                {
                    Assert.NotNull(c.ActionContext);
                    Assert.Equal(expectedSeeds, seeds);
                })
                .Returns(expander1Result)
                .Verifiable();

            var expander2 = new Mock<IViewLocationExpander>();
            expander2
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
                {
                    Assert.Equal(expander1Result, seeds);
                })
                .Returns(new[] { "test-string/{1}.cshtml" })
                .Verifiable();

            var viewEngine = CreateViewEngine(
                pageFactory.Object,
                new[] { expander1.Object, expander2.Object });
            var context = GetActionContext(routeValues);

            // Act
            var result = viewEngine.FindView(context, "test-view", isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.IsAssignableFrom<IView>(result.View);
            pageFactory.Verify();
            expander1.Verify();
            expander2.Verify();
        }

        [Fact]
        public void FindView_UsesViewLocationExpandersToLocateViews_ForAreas()
        {
            // Arrange
            var routeValues = _areaTestContext;
            var expectedSeeds = new[]
            {
                "/Areas/{2}/Views/{1}/{0}.cshtml",
                "/Areas/{2}/Views/Shared/{0}.cshtml",
                "/Views/Shared/{0}.cshtml"
            };

            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(p => p.CreateFactory("test-string/bar.cshtml"))
                .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
                .Verifiable();

            var expander1Result = new[] { "some-seed" };
            var expander1 = new Mock<IViewLocationExpander>();
            expander1
                .Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
                .Callback((ViewLocationExpanderContext c) =>
                {
                    Assert.NotNull(c.ActionContext);
                    c.Values["expander-key"] = expander1.ToString();
                })
                .Verifiable();
            expander1
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
                {
                    Assert.NotNull(c.ActionContext);
                    Assert.Equal(expectedSeeds, seeds);
                })
                .Returns(expander1Result)
                .Verifiable();

            var expander2 = new Mock<IViewLocationExpander>();
            expander2
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
                {
                    Assert.Equal(expander1Result, seeds);
                })
                .Returns(new[] { "test-string/{1}.cshtml" })
                .Verifiable();

            var viewEngine = CreateViewEngine(
                pageFactory.Object,
                new[] { expander1.Object, expander2.Object });
            var context = GetActionContext(routeValues);

            // Act
            var result = viewEngine.FindView(context, "test-view", isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.IsAssignableFrom<IView>(result.View);
            pageFactory.Verify();
            expander1.Verify();
            expander2.Verify();
        }

        [Fact]
        public void FindView_NormalizesPaths_ReturnedByViewLocationExpanders()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(p => p.CreateFactory(@"Views\Home\Index.cshtml"))
                .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
                .Verifiable();

            var expander = new Mock<IViewLocationExpander>();
            expander
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Returns(new[] { @"Views\Home\Index.cshtml" });

            var viewEngine = CreateViewEngine(
                pageFactory.Object,
                new[] { expander.Object });
            var context = GetActionContext(new Dictionary<string, object>());

            // Act
            var result = viewEngine.FindView(context, "test-view", isMainPage: true);

            // Assert
            Assert.True(result.Success);
            Assert.IsAssignableFrom<IView>(result.View);
            pageFactory.Verify();
        }

        [Fact]
        public void FindView_CachesValuesIfViewWasFound()
        {
            // Arrange
            var page = Mock.Of<IRazorPage>();
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
                .Returns(GetPageFactoryResult(factory: null))
                .Verifiable();
            pageFactory
               .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
               .Returns(GetPageFactoryResult(() => page))
               .Verifiable();

            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(_controllerTestContext);

            // Act 1
            var result1 = viewEngine.FindView(context, "baz", isMainPage: true);

            // Assert 1
            Assert.True(result1.Success);
            var view1 = Assert.IsType<RazorView>(result1.View);
            Assert.Same(page, view1.RazorPage);
            pageFactory.Verify();

            // Act 2
            pageFactory
               .Setup(p => p.CreateFactory(It.IsAny<string>()))
               .Throws(new Exception("Shouldn't be called"));

            var result2 = viewEngine.FindView(context, "baz", isMainPage: true);

            // Assert 2
            Assert.True(result2.Success);
            var view2 = Assert.IsType<RazorView>(result2.View);
            Assert.Same(page, view2.RazorPage);
            pageFactory.Verify();
        }

        [Fact]
        public void FindView_CachesValuesIfViewWasFound_ForPages()
        {
            // Arrange
            var page = Mock.Of<IRazorPage>();
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
               .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
               .Returns(GetPageFactoryResult(() => page))
               .Verifiable();

            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(_pageTestContext);

            // Act 1
            var result1 = viewEngine.FindView(context, "baz", isMainPage: false);

            // Assert 1
            Assert.True(result1.Success);
            var view1 = Assert.IsType<RazorView>(result1.View);
            Assert.Same(page, view1.RazorPage);
            pageFactory.Verify();

            // Act 2
            pageFactory
               .Setup(p => p.CreateFactory(It.IsAny<string>()))
               .Throws(new Exception("Shouldn't be called"));

            var result2 = viewEngine.FindView(context, "baz", isMainPage: false);

            // Assert 2
            Assert.True(result2.Success);
            var view2 = Assert.IsType<RazorView>(result2.View);
            Assert.Same(page, view2.RazorPage);
            pageFactory.Verify();
        }

        [Fact]
        public void FindView_InvokesPageFactoryIfChangeTokenExpired()
        {
            // Arrange
            var page1 = Mock.Of<IRazorPage>();
            var page2 = Mock.Of<IRazorPage>();
            var sequence = new MockSequence();
            var cancellationTokenSource = new CancellationTokenSource();
            var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);

            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .InSequence(sequence)
                .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
                .Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken }));
            pageFactory
               .InSequence(sequence)
               .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
               .Returns(GetPageFactoryResult(() => page1))
               .Verifiable();
            pageFactory
                .InSequence(sequence)
                .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
                .Returns(GetPageFactoryResult(() => page2));

            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(_controllerTestContext);

            // Act 1
            var result1 = viewEngine.FindView(context, "baz", isMainPage: true);

            // Assert 1
            Assert.True(result1.Success);
            var view1 = Assert.IsType<RazorView>(result1.View);
            Assert.Same(page1, view1.RazorPage);

            // Act 2
            cancellationTokenSource.Cancel();
            var result2 = viewEngine.FindView(context, "baz", isMainPage: true);

            // Assert 2
            Assert.True(result2.Success);
            var view2 = Assert.IsType<RazorView>(result2.View);
            Assert.Same(page2, view2.RazorPage);
            pageFactory.Verify();
        }

        // This test validates an important perf scenario of RazorViewEngine not constructing
        // multiple strings for views that do not exist in the file system on a per-request basis.
        [Fact]
        public void FindView_DoesNotInvokeViewLocationExpanders_IfChangeTokenHasNotExpired()
        {
            // Arrange
            var pageFactory = Mock.Of<IRazorPageFactoryProvider>();
            var expander = new Mock<IViewLocationExpander>();
            var expandedLocations = new[]
            {
                "viewlocation1",
                "viewlocation2",
                "viewlocation3",
            };
            expander
                .Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
                .Callback((ViewLocationExpanderContext expanderContext) =>
                {
                    expanderContext.Values["somekey"] = "somevalue";
                })
                .Verifiable();
            expander
                .Setup(v => v.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Returns(expandedLocations)
                .Verifiable();

            var viewEngine = CreateViewEngine(
                pageFactory,
                expanders: new[] { expander.Object });
            var context = GetActionContext(_controllerTestContext);

            // Act - 1
            var result = viewEngine.FindView(context, "myview", isMainPage: true);

            // Assert - 1
            Assert.False(result.Success);
            Assert.Equal(expandedLocations, result.SearchedLocations);
            expander.Verify();

            // Act - 2
            result = viewEngine.FindView(context, "myview", isMainPage: true);

            // Assert - 2
            Assert.False(result.Success);
            Assert.Equal(expandedLocations, result.SearchedLocations);
            expander.Verify(
                v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()),
                Times.Exactly(2));
            expander.Verify(
                v => v.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>()),
                Times.Once());
        }

        [Fact]
        public void FindView_InvokesViewLocationExpanders_IfChangeTokenExpires()
        {
            // Arrange
            var cancellationTokenSource = new CancellationTokenSource();
            var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
            var page = Mock.Of<IRazorPage>();
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(p => p.CreateFactory("viewlocation3"))
                .Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken }));
            var expander = new Mock<IViewLocationExpander>();
            var expandedLocations = new[]
            {
                "viewlocation1",
                "viewlocation2",
                "viewlocation3",
            };
            expander
                .Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
                .Callback((ViewLocationExpanderContext expanderContext) =>
                {
                    expanderContext.Values["somekey"] = "somevalue";
                })
                .Verifiable();
            expander
                .Setup(v => v.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Returns(expandedLocations)
                .Verifiable();

            var viewEngine = CreateViewEngine(
                pageFactory.Object,
                expanders: new[] { expander.Object });
            var context = GetActionContext(_controllerTestContext);

            // Act - 1
            var result = viewEngine.FindView(context, "MyView", isMainPage: true);

            // Assert - 1
            Assert.False(result.Success);
            Assert.Equal(expandedLocations, result.SearchedLocations);
            expander.Verify();

            // Act - 2
            pageFactory
                .Setup(p => p.CreateFactory("viewlocation3"))
                .Returns(GetPageFactoryResult(() => page));
            cancellationTokenSource.Cancel();
            result = viewEngine.FindView(context, "MyView", isMainPage: true);

            // Assert - 2
            Assert.True(result.Success);
            var view = Assert.IsType<RazorView>(result.View);
            Assert.Same(page, view.RazorPage);
            expander.Verify(
                v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()),
                Times.Exactly(2));
            expander.Verify(
                v => v.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>()),
                Times.Exactly(2));
        }

        [Theory]
        [MemberData(nameof(AbsoluteViewPathData))]
        public void FindPage_WithFullPath_ReturnsNotFound(string viewName)
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindPage(context, viewName);

            // Assert
            Assert.Null(result.Page);
        }

        [Theory]
        [MemberData(nameof(AbsoluteViewPathData))]
        public void FindPage_WithFullPathAndCshtmlEnding_ReturnsNotFound(string viewName)
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();
            var context = GetActionContext(_controllerTestContext);
            viewName += ".cshtml";

            // Act
            var result = viewEngine.FindPage(context, viewName);

            // Assert
            Assert.Null(result.Page);
        }

        [Fact]
        public void FindPage_WithRelativePath_ReturnsNotFound()
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindPage(context, "View.cshtml");

            // Assert
            Assert.Null(result.Page);
        }

        [Fact]
        public void GetPage_WithViewName_ReturnsNotFound()
        {
            // Arrange
            var viewEngine = CreateSuccessfulViewEngine();

            // Act
            var result = viewEngine.GetPage("~/Home/View1.cshtml", "View2");

            // Assert
            Assert.Null(result.Page);
        }

        [Theory]
        [InlineData(null)]
        [InlineData("")]
        public void FindPage_ThrowsIfNameIsNullOrEmpty(string pageName)
        {
            // Arrange
            var viewEngine = CreateViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act & Assert
            ExceptionAssert.ThrowsArgumentNullOrEmpty(
                () => viewEngine.FindPage(context, pageName),
                "pageName");
        }

        [Fact]
        public void FindPage_UsesViewLocationExpander_ToExpandPaths()
        {
            // Arrange
            var routeValues = _controllerTestContext;
            var expectedSeeds = new[]
            {
                "/Views/{1}/{0}.cshtml",
                "/Views/Shared/{0}.cshtml"
            };

            var page = Mock.Of<IRazorPage>();
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(p => p.CreateFactory("expanded-path/bar-layout"))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();

            var expander = new Mock<IViewLocationExpander>();
            expander
                .Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
                .Callback((ViewLocationExpanderContext c) =>
                {
                    Assert.NotNull(c.ActionContext);
                    c.Values["expander-key"] = expander.ToString();
                })
                .Verifiable();
            expander
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Returns((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
                {
                    Assert.NotNull(c.ActionContext);
                    Assert.Equal(expectedSeeds, seeds);

                    Assert.Equal(expander.ToString(), c.Values["expander-key"]);

                    return new[] { "expanded-path/bar-{0}" };
                })
                .Verifiable();

            var viewEngine = CreateViewEngine(
                pageFactory.Object,
                new[] { expander.Object });
            var context = GetActionContext(routeValues);

            // Act
            var result = viewEngine.FindPage(context, "layout");

            // Assert
            Assert.Equal("layout", result.Name);
            Assert.Same(page, result.Page);
            Assert.Null(result.SearchedLocations);
            pageFactory.Verify();
            expander.Verify();
        }

        [Fact]
        public void FindPage_UsesViewLocationExpander_ToExpandPaths_ForAreas()
        {
            // Arrange
            var routeValues = _areaTestContext;
            var expectedSeeds = new[]
            {
                "/Areas/{2}/Views/{1}/{0}.cshtml",
                "/Areas/{2}/Views/Shared/{0}.cshtml",
                "/Views/Shared/{0}.cshtml"
            };

            var page = Mock.Of<IRazorPage>();
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(p => p.CreateFactory("expanded-path/bar-layout"))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();

            var expander = new Mock<IViewLocationExpander>();
            expander
                .Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
                .Callback((ViewLocationExpanderContext c) =>
                {
                    Assert.NotNull(c.ActionContext);
                    c.Values["expander-key"] = expander.ToString();
                })
                .Verifiable();
            expander
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Returns((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
                {
                    Assert.NotNull(c.ActionContext);
                    Assert.Equal(expectedSeeds, seeds);

                    Assert.Equal(expander.ToString(), c.Values["expander-key"]);

                    return new[] { "expanded-path/bar-{0}" };
                })
                .Verifiable();

            var viewEngine = CreateViewEngine(
                pageFactory.Object,
                new[] { expander.Object });
            var context = GetActionContext(routeValues);

            // Act
            var result = viewEngine.FindPage(context, "layout");

            // Assert
            Assert.Equal("layout", result.Name);
            Assert.Same(page, result.Page);
            Assert.Null(result.SearchedLocations);
            pageFactory.Verify();
            expander.Verify();
        }

        [Fact]
        public void FindPage_ReturnsSearchedLocationsIfPageCannotBeFound()
        {
            // Arrange
            var expected = new[]
            {
                "/Views/bar/layout.cshtml",
                "/Views/Shared/layout.cshtml",
            };

            var viewEngine = CreateViewEngine();
            var context = GetActionContext(_controllerTestContext);

            // Act
            var result = viewEngine.FindPage(context, "layout");

            // Assert
            Assert.Equal("layout", result.Name);
            Assert.Null(result.Page);
            Assert.Equal(expected, result.SearchedLocations);
        }

        [Fact]
        public void FindPage_SelectsActionCaseInsensitively()
        {
            // The ActionDescriptor contains "Foo" and the RouteData contains "foo"
            // which matches the case of the constructor thus searching in the appropriate location.
            // Arrange
            var routeValues = new Dictionary<string, object>
            {
                { "controller", "foo" }
            };
            var page = new Mock<IRazorPage>(MockBehavior.Strict);
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory
                .Setup(p => p.CreateFactory("/Views/Foo/details.cshtml"))
                .Returns(GetPageFactoryResult(() => page.Object))
                .Verifiable();

            var viewEngine = CreateViewEngine(pageFactory.Object);
            var routesInActionDescriptor = new Dictionary<string, string>()
            {
                { "controller", "Foo" }
            };

            var context = GetActionContextWithActionDescriptor(
                routeValues,
                routesInActionDescriptor);

            // Act
            var result = viewEngine.FindPage(context, "details");

            // Assert
            Assert.Equal("details", result.Name);
            Assert.Same(page.Object, result.Page);
            Assert.Null(result.SearchedLocations);
            pageFactory.Verify();
        }

        [Fact]
        public void FindPage_LooksForPages_UsingActionDescriptor_Controller()
        {
            // Arrange
            var expected = new[]
            {
                "/Views/bar/foo.cshtml",
                "/Views/Shared/foo.cshtml",
            };

            var routeValues = new Dictionary<string, object>
            {
                { "controller", "Bar" }
            };
            var routesInActionDescriptor = new Dictionary<string, string>()
            {
                { "controller", "bar" }
            };

            var viewEngine = CreateViewEngine();
            var context = GetActionContextWithActionDescriptor(
                routeValues,
                routesInActionDescriptor);

            // Act
            var result = viewEngine.FindPage(context, "foo");

            // Assert
            Assert.Equal("foo", result.Name);
            Assert.Null(result.Page);
            Assert.Equal(expected, result.SearchedLocations);
        }

        [Fact]
        public void FindPage_LooksForPages_UsingActionDescriptor_Areas()
        {
            // Arrange
            var expected = new[]
            {
                "/Areas/world/Views/bar/foo.cshtml",
                "/Areas/world/Views/Shared/foo.cshtml",
                "/Views/Shared/foo.cshtml"
            };

            var routeValues = new Dictionary<string, object>
            {
                { "controller", "Bar" },
                { "area", "World" }
            };
            var routesInActionDescriptor = new Dictionary<string, string>()
            {
                { "controller", "bar" },
                { "area", "world" }
            };

            var viewEngine = CreateViewEngine();
            var context = GetActionContextWithActionDescriptor(
                routeValues,
                routesInActionDescriptor);

            // Act
            var result = viewEngine.FindPage(context, "foo");

            // Assert
            Assert.Equal("foo", result.Name);
            Assert.Null(result.Page);
            Assert.Equal(expected, result.SearchedLocations);
        }

        [Fact]
        public void FindPage_LooksForPages_UsesRouteValuesAsFallback()
        {
            // Arrange
            var expected = new[]
            {
                "/Views/foo/bar.cshtml",
                "/Views/Shared/bar.cshtml",
            };

            var routeValues = new Dictionary<string, object>()
            {
                { "controller", "foo" }
            };

            var viewEngine = CreateViewEngine();
            var context = GetActionContextWithActionDescriptor(
                routeValues,
                new Dictionary<string, string>());

            // Act
            var result = viewEngine.FindPage(context, "bar");

            // Assert
            Assert.Equal("bar", result.Name);
            Assert.Null(result.Page);
            Assert.Equal(expected, result.SearchedLocations);
        }

        [Theory]
        [InlineData("/Test-View.cshtml")]
        [InlineData("~/Test-View.CSHTML")]
        [InlineData("/Home/Test-View.CSHTML")]
        [InlineData("~/Home/Test-View.cshtml")]
        [InlineData("~/SHARED/TEST-VIEW.CSHTML")]
        public void GetPage_UsesGivenPath_WithAppRelativePath(string pageName)
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(pageName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act
            var result = viewEngine.GetPage("~/Another/Place.cshtml", pagePath: pageName);

            // Assert
            Assert.Same(page, result.Page);
            Assert.Equal(pageName, result.Name);
            pageFactory.Verify();
        }

        [Theory]
        [InlineData("Test-View.cshtml")]
        [InlineData("Test-View.CSHTML")]
        [InlineData("PATH/TEST-VIEW.CSHTML")]
        [InlineData("Path1/Path2/Test-View.cshtml")]
        public void GetPage_ResolvesRelativeToCurrentPage_WithRelativePath(string pageName)
        {
            // Arrange
            var expectedPageName = $"/Home/{ pageName }";
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(expectedPageName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act
            var result = viewEngine.GetPage("/Home/Page.cshtml", pageName);

            // Assert
            Assert.Same(page, result.Page);
            Assert.Equal(pageName, result.Name);
            pageFactory.Verify();
        }

        [Theory]
        [InlineData("Test-View.cshtml")]
        [InlineData("Test-View.CSHTML")]
        [InlineData("PATH/TEST-VIEW.CSHTML")]
        [InlineData("Path1/Path2/Test-View.cshtml")]
        public void GetPage_ResolvesRelativeToAppRoot_WithRelativePath_IfNoPageExecuting(string pageName)
        {
            // Arrange
            var expectedPageName = $"/{ pageName }";
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            pageFactory
                .Setup(p => p.CreateFactory(expectedPageName))
                .Returns(GetPageFactoryResult(() => page))
                .Verifiable();
            var viewEngine = new TestableRazorViewEngine(
                pageFactory.Object,
                GetOptionsAccessor());

            // Act
            var result = viewEngine.GetPage(executingFilePath: null, pagePath: pageName);

            // Assert
            Assert.Same(page, result.Page);
            Assert.Equal(pageName, result.Name);
            pageFactory.Verify();
        }

        [Theory]
        [InlineData(null, null)]
        [InlineData(null, "")]
        [InlineData(null, "Page")]
        [InlineData(null, "Folder/Page")]
        [InlineData(null, "Folder1/Folder2/Page")]
        [InlineData("/Home/Index.cshtml", null)]
        [InlineData("/Home/Index.cshtml", "")]
        [InlineData("/Home/Index.cshtml", "Page")]
        [InlineData("/Home/Index.cshtml", "Folder/Page")]
        [InlineData("/Home/Index.cshtml", "Folder1/Folder2/Page")]
        public void GetAbsolutePath_ReturnsPagePathUnchanged_IfNotAPath(string executingFilePath, string pagePath)
        {
            // Arrange
            var viewEngine = CreateViewEngine();

            // Act
            var result = viewEngine.GetAbsolutePath(executingFilePath, pagePath);

            // Assert
            Assert.Same(pagePath, result);
        }

        [Theory]
        [InlineData("/Views/Home/Index.cshtml", "../Shared/_Partial.cshtml")]
        [InlineData("/Views/Home/Index.cshtml", "..\\Shared\\_Partial.cshtml")]
        [InlineData("/Areas/MyArea/Views/Home/Index.cshtml", "../../../../Views/Shared/_Partial.cshtml")]
        [InlineData("/Views/Accounts/Users.cshtml", "../Test/../Shared/_Partial.cshtml")]
        [InlineData("Views/Accounts/Users.cshtml", "./../Shared/./_Partial.cshtml")]
        public void GetAbsolutePath_ResolvesPathTraversals(string executingFilePath, string pagePath)
        {
            // Arrange
            var viewEngine = CreateViewEngine();

            // Act
            var result = viewEngine.GetAbsolutePath(executingFilePath, pagePath);

            // Assert
            Assert.Equal("/Views/Shared/_Partial.cshtml", result);
        }

        [Theory]
        [InlineData("../Shared/_Layout.cshtml")]
        [InlineData("Folder1/../Folder2/../../File.cshtml")]
        public void GetAbsolutePath_DoesNotResolvePathIfTraversalsEscapeTheRoot(string pagePath)
        {
            // Arrange
            var expected = '/' + pagePath;
            var viewEngine = CreateViewEngine();

            // Act
            var result = viewEngine.GetAbsolutePath("/Index.cshtml", pagePath);

            // Assert
            Assert.Equal(expected, result);
        }

        [Theory]
        [InlineData(null, "/Page")]
        [InlineData(null, "~/Folder/Page.cshtml")]
        [InlineData(null, "/Folder1/Folder2/Page.rzr")]
        [InlineData("/Home/Index.cshtml", "~/Page")]
        [InlineData("/Home/Index.cshtml", "/Folder/Page.cshtml")]
        [InlineData("/Home/Index.cshtml", "~/Folder1/Folder2/Page.rzr")]
        public void GetAbsolutePath_ReturnsPagePathUnchanged_IfAppRelative(string executingFilePath, string pagePath)
        {
            // Arrange
            var viewEngine = CreateViewEngine();

            // Act
            var result = viewEngine.GetAbsolutePath(executingFilePath, pagePath);

            // Assert
            Assert.Same(pagePath, result);
        }

        [Theory]
        [InlineData("Page.cshtml")]
        [InlineData("Folder/Page.cshtml")]
        [InlineData("../../Folder1/Folder2/Page.cshtml")]
        public void GetAbsolutePath_ResolvesRelativeToExecutingPage(string pagePath)
        {
            // Arrange
            var expectedPagePath = "/Home/" + pagePath;
            var viewEngine = CreateViewEngine();

            // Act
            var result = viewEngine.GetAbsolutePath("/Home/Page.cshtml", pagePath);

            // Assert
            Assert.Equal(expectedPagePath, result);
        }

        [Theory]
        [InlineData("Page.cshtml")]
        [InlineData("Folder/Page.cshtml")]
        [InlineData("../../Folder1/Folder2/Page.cshtml")]
        public void GetAbsolutePath_ResolvesRelativeToAppRoot_IfNoPageExecuting(string pagePath)
        {
            // Arrange
            var expectedPagePath = "/" + pagePath;
            var viewEngine = CreateViewEngine();

            // Act
            var result = viewEngine.GetAbsolutePath(executingFilePath: null, pagePath: pagePath);

            // Assert
            Assert.Equal(expectedPagePath, result);
        }

        [Fact]
        public void GetNormalizedRouteValue_ReturnsValueFromRouteValues()
        {
            // Arrange
            var key = "some-key";
            var actionDescriptor = new ActionDescriptor();
            actionDescriptor.RouteValues.Add(key, "Route-Value");

            var actionContext = new ActionContext
            {
                ActionDescriptor = actionDescriptor,
                RouteData = new RouteData()
            };

            actionContext.RouteData.Values[key] = "route-value";

            // Act
            var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);

            // Assert
            Assert.Equal("Route-Value", result);
        }

        [Fact]
        [ReplaceCulture("de-CH", "de-CH")]
        public void GetNormalizedRouteValue_UsesInvariantCulture()
        {
            // Arrange
            var key = "some-key";
            var actionDescriptor = new ActionDescriptor();
            actionDescriptor.RouteValues.Add(key, "Route-Value");

            var actionContext = new ActionContext
            {
                ActionDescriptor = actionDescriptor,
                RouteData = new RouteData()
            };

            actionContext.RouteData.Values[key] = new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7));

            // Act
            var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);

            // Assert
            Assert.Equal("10/31/2018 07:37:38 -07:00", result);
        }

        [Fact]
        public void GetNormalizedRouteValue_ReturnsRouteValue_IfValueDoesNotMatch()
        {
            // Arrange
            var key = "some-key";
            var actionDescriptor = new ActionDescriptor();
            actionDescriptor.RouteValues.Add(key, "different-value");

            var actionContext = new ActionContext
            {
                ActionDescriptor = actionDescriptor,
                RouteData = new RouteData()
            };

            actionContext.RouteData.Values[key] = "route-value";

            // Act
            var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);

            // Assert
            Assert.Equal("route-value", result);
        }

        [Fact]
        public void GetNormalizedRouteValue_ReturnsNonNormalizedValue_IfActionRouteValueIsNull()
        {
            // Arrange
            var key = "some-key";
            var actionDescriptor = new ActionDescriptor();
            actionDescriptor.RouteValues.Add(key, null);

            var actionContext = new ActionContext
            {
                ActionDescriptor = actionDescriptor,
                RouteData = new RouteData()
            };

            actionContext.RouteData.Values[key] = "route-value";

            // Act
            var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);

            // Assert
            Assert.Equal("route-value", result);
        }

        [Fact]
        public void GetNormalizedRouteValue_ConvertsRouteValueToString()
        {
            using (new CultureReplacer())
            {
                // Arrange
                var key = "some-key";
                var actionDescriptor = new ActionDescriptor
                {
                    AttributeRouteInfo = new AttributeRouteInfo(),
                };

                var actionContext = new ActionContext
                {
                    ActionDescriptor = actionDescriptor,
                    RouteData = new RouteData()
                };

                actionContext.RouteData.Values[key] = 43;

                // Act
                var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);

                // Assert
                Assert.Equal("43", result);
            }
        }

        [Fact]
        public void GetViewLocationFormats_ForControllerWithoutArea_ReturnsDefaultSet()
        {
            // Arrange
            var expected = new string[] { "expected", };

            var viewEngine = new TestableRazorViewEngine(
                Mock.Of<IRazorPageFactoryProvider>(),
                GetOptionsAccessor(viewLocationFormats: expected));

            var context = new ViewLocationExpanderContext(
                new ActionContext(),
                "Index.cshtml",
                controllerName: "Home",
                areaName: null,
                pageName: "ignored",
                isMainPage: true);

            // Act
            var actual = viewEngine.GetViewLocationFormats(context);

            // Assert
            Assert.Equal(expected, actual);
        }

        [Fact]
        public void GetViewLocationFormats_ForControllerWithArea_ReturnsAreaSet()
        {
            // Arrange
            var expected = new string[] { "expected", };

            var viewEngine = new TestableRazorViewEngine(
                Mock.Of<IRazorPageFactoryProvider>(),
                GetOptionsAccessor(areaViewLocationFormats: expected));

            var context = new ViewLocationExpanderContext(
                new ActionContext(),
                "Index.cshtml",
                controllerName: "Home",
                areaName: "Admin",
                pageName: "ignored",
                isMainPage: true);

            // Act
            var actual = viewEngine.GetViewLocationFormats(context);

            // Assert
            Assert.Equal(expected, actual);
        }

        [Fact]
        public void GetViewLocationFormats_ForPage_ReturnsPageSet()
        {
            // Arrange
            var expected = new string[] { "expected", };

            var viewEngine = new TestableRazorViewEngine(
                Mock.Of<IRazorPageFactoryProvider>(),
                GetOptionsAccessor(pageViewLocationFormats: expected));

            var context = new ViewLocationExpanderContext(
                new ActionContext(),
                "Index.cshtml",
                controllerName: null,
                areaName: null,
                pageName: "/Some/Page",
                isMainPage: true);

            // Act
            var actual = viewEngine.GetViewLocationFormats(context);

            // Assert
            Assert.Equal(expected, actual);
        }

        // This isn't a real case we expect to hit in an app, just making sure we have a reasonable default
        // for a weird configuration. In this case we preserve what we did in 1.0.0.
        [Fact]
        public void GetViewLocationFormats_NoRouteValues_ReturnsDefaultSet()
        {
            // Arrange
            var expected = new string[] { "expected", };

            var viewEngine = new TestableRazorViewEngine(
                Mock.Of<IRazorPageFactoryProvider>(),
                GetOptionsAccessor(viewLocationFormats: expected));

            var context = new ViewLocationExpanderContext(
                new ActionContext(),
                "Index.cshtml",
                controllerName: null,
                areaName: null,
                pageName: null,
                isMainPage: true);

            // Act
            var actual = viewEngine.GetViewLocationFormats(context);

            // Assert
            Assert.Equal(expected, actual);
        }

        [Fact]
        public void ViewEngine_DoesNotSetPageValue_IfItIsNotSpecifiedInRouteValues()
        {
            // Arrange
            var routeValues = new Dictionary<string, object>
            {
                { "controller", "MyController" },
                { "action", "MyAction" }
            };

            var expected = new[] { "some-seed" };
            var expander = new Mock<IViewLocationExpander>();
            expander
                .Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
                .Callback((ViewLocationExpanderContext c) =>
                {
                    Assert.Equal("MyController", c.ControllerName);
                    Assert.Null(c.PageName);
                })
                .Verifiable();

            expander
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Returns(expected);

            var viewEngine = CreateViewEngine(expanders: new[] { expander.Object });
            var context = GetActionContext(routeValues);

            // Act
            viewEngine.FindView(context, viewName: "Test-view", isMainPage: true);

            // Assert
            expander.Verify();
        }

        [Fact]
        public void ViewEngine_SetsPageValue_IfItIsSpecifiedInRouteValues()
        {
            // Arrange
            var routeValues = new Dictionary<string, object>
            {
                { "page", "MyPage" },
            };

            var expected = new[] { "some-seed" };
            var expander = new Mock<IViewLocationExpander>();
            expander
                .Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
                .Callback((ViewLocationExpanderContext c) =>
                {
                    Assert.Equal("MyPage", c.PageName);
                })
                .Verifiable();

            expander
                .Setup(e => e.ExpandViewLocations(
                    It.IsAny<ViewLocationExpanderContext>(),
                    It.IsAny<IEnumerable<string>>()))
                .Returns(expected);

            var viewEngine = CreateViewEngine(expanders: new[] { expander.Object });
            var context = GetActionContext(routeValues);
            context.ActionDescriptor.RouteValues["page"] = "MyPage";

            // Act
            viewEngine.FindView(context, viewName: "MyView", isMainPage: true);

            // Assert
            expander.Verify();
        }

        [Fact]
        public void FindView_ResolvesDirectoryTraversalsPriorToInvokingPageFactory()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory.Setup(p => p.CreateFactory("/Views/Shared/_Partial.cshtml"))
                .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
                .Verifiable();
            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(new Dictionary<string, object>());

            // Act
            var result = viewEngine.FindView(context, "../Shared/_Partial", isMainPage: false);

            // Assert
            pageFactory.Verify();
        }

        [Fact]
        public void FindPage_ResolvesDirectoryTraversalsPriorToInvokingPageFactory()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory.Setup(p => p.CreateFactory("/Views/Shared/_Partial.cshtml"))
                .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
                .Verifiable();
            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(new Dictionary<string, object>());

            // Act
            var result = viewEngine.FindPage(context, "../Shared/_Partial");

            // Assert
            pageFactory.Verify();
        }

        // Tests to verify fix for https://github.com/aspnet/Mvc/issues/6672
        // Without normalizing the path, the view engine would have attempted to lookup "/Views//MyView.cshtml"
        // which works for PhysicalFileProvider but fails for exact lookups performed during precompilation.
        // We normalize it to "/Views/MyView.cshtml" to avoid this discrepancy.
        [Fact]
        public void FindView_ResolvesNormalizesSlashesPriorToInvokingPageFactory()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory.Setup(p => p.CreateFactory("/Views/MyView.cshtml"))
                .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
                .Verifiable();
            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(new Dictionary<string, object>());

            // Act
            var result = viewEngine.FindView(context, "MyView", isMainPage: true);

            // Assert
            pageFactory.Verify();
        }

        [Fact]
        public void FindPage_ResolvesNormalizesSlashesPriorToInvokingPageFactory()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            pageFactory.Setup(p => p.CreateFactory("/Views/MyPage.cshtml"))
                .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
                .Verifiable();
            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(new Dictionary<string, object>());

            // Act
            var result = viewEngine.FindPage(context, "MyPage");

            // Assert
            pageFactory.Verify();
        }

        [Fact]
        public void FindView_Succeeds_AfterClearCache()
        {
            // Arrange
            var pageFactory = new Mock<IRazorPageFactoryProvider>();
            var page = Mock.Of<IRazorPage>();
            var hotReloadedPage = Mock.Of<IRazorPage>();

            pageFactory
                .Setup(p => p.CreateFactory("/Views/bar/Index.cshtml"))
                .Returns(GetPageFactoryResult(() => page));

            var viewEngine = CreateViewEngine(pageFactory.Object);
            var context = GetActionContext(_controllerTestContext);

            // Act - 1
            // First verify results are typically cached
            for (var i = 0; i < 3; i++)
            {

                var result = viewEngine.FindView(context, "Index", isMainPage: false);

                // Assert - 1
                Assert.True(result.Success);
                var view = Assert.IsType<RazorView>(result.View);
                Assert.Same(page, view.RazorPage);

                pageFactory.Verify(p => p.CreateFactory("/Views/bar/Index.cshtml"), Times.Once());
            }

            // Now verify that the page factory is re-invoked after cache clear
            viewEngine.ClearCache();
            pageFactory
                .Setup(p => p.CreateFactory("/Views/bar/Index.cshtml"))
                .Returns(GetPageFactoryResult(() => hotReloadedPage));

            {
                // Act - 2
                var result = viewEngine.FindView(context, "Index", isMainPage: false);

                // Assert - 2
                Assert.True(result.Success);
                var view = Assert.IsType<RazorView>(result.View);
                Assert.Same(hotReloadedPage, view.RazorPage);

                pageFactory.Verify(p => p.CreateFactory("/Views/bar/Index.cshtml"), Times.Exactly(2));
            }
        }

        // Return RazorViewEngine with a page factory provider that is always successful.
        private RazorViewEngine CreateSuccessfulViewEngine()
        {
            var pageFactory = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict);
            pageFactory
                .Setup(f => f.CreateFactory(It.IsAny<string>()))
                .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()));

            return CreateViewEngine(pageFactory.Object);
        }

        private static RazorPageFactoryResult GetPageFactoryResult(
            Func<IRazorPage> factory,
            IList<IChangeToken> changeTokens = null,
            string path = "/Views/Home/Index.cshtml")
        {
            var descriptor = new CompiledViewDescriptor
            {
                ExpirationTokens = changeTokens ?? Array.Empty<IChangeToken>(),
                RelativePath = path,
            };

            return new RazorPageFactoryResult(descriptor, factory);
        }

        private TestableRazorViewEngine CreateViewEngine(
            IRazorPageFactoryProvider pageFactory = null,
            IEnumerable<IViewLocationExpander> expanders = null)
        {
            pageFactory = pageFactory ?? Mock.Of<IRazorPageFactoryProvider>();
            return new TestableRazorViewEngine(pageFactory, GetOptionsAccessor(expanders));
        }

        private static IOptions<RazorViewEngineOptions> GetOptionsAccessor(
            IEnumerable<IViewLocationExpander> expanders = null,
            IEnumerable<string> viewLocationFormats = null,
            IEnumerable<string> areaViewLocationFormats = null,
            IEnumerable<string> pageViewLocationFormats = null)
        {
            var optionsSetup = new RazorViewEngineOptionsSetup();

            var options = new RazorViewEngineOptions();
            optionsSetup.Configure(options);
            options.PageViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
            options.PageViewLocationFormats.Add("/Pages/Shared/{0}.cshtml");

            if (expanders != null)
            {
                foreach (var expander in expanders)
                {
                    options.ViewLocationExpanders.Add(expander);
                }
            }

            if (viewLocationFormats != null)
            {
                options.ViewLocationFormats.Clear();

                foreach (var location in viewLocationFormats)
                {
                    options.ViewLocationFormats.Add(location);
                }
            }

            if (areaViewLocationFormats != null)
            {
                options.AreaViewLocationFormats.Clear();

                foreach (var location in areaViewLocationFormats)
                {
                    options.AreaViewLocationFormats.Add(location);
                }
            }

            if (pageViewLocationFormats != null)
            {
                options.PageViewLocationFormats.Clear();

                foreach (var location in pageViewLocationFormats)
                {
                    options.PageViewLocationFormats.Add(location);
                }
            }

            var optionsAccessor = new Mock<IOptions<RazorViewEngineOptions>>();
            optionsAccessor
                .SetupGet(v => v.Value)
                .Returns(options);
            return optionsAccessor.Object;
        }

        private static ActionContext GetActionContext(IDictionary<string, object> routeValues)
        {
            var httpContext = new DefaultHttpContext();
            var routeData = new RouteData();
            foreach (var kvp in routeValues)
            {
                routeData.Values.Add(kvp.Key, kvp.Value);
            }

            var actionDescriptor = new ActionDescriptor();
            return new ActionContext(httpContext, routeData, actionDescriptor);
        }

        private static ActionContext GetActionContextWithActionDescriptor(
            IDictionary<string, object> routeValues,
            IDictionary<string, string> actionRouteValues)
        {
            var httpContext = new DefaultHttpContext();
            var routeData = new RouteData();
            foreach (var kvp in routeValues)
            {
                routeData.Values.Add(kvp.Key, kvp.Value);
            }

            var actionDescriptor = new ActionDescriptor();

            foreach (var kvp in actionRouteValues)
            {
                actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
            }

            return new ActionContext(httpContext, routeData, actionDescriptor);
        }

        private class TestableRazorViewEngine : RazorViewEngine
        {
            public TestableRazorViewEngine(
                IRazorPageFactoryProvider pageFactory,
                IOptions<RazorViewEngineOptions> optionsAccessor)
                : base(pageFactory, Mock.Of<IRazorPageActivator>(), new HtmlTestEncoder(), optionsAccessor, NullLoggerFactory.Instance, new DiagnosticListener("Microsoft.AspNetCore.Mvc.Razor"))
            {
            }

            public IMemoryCache ViewLookupCachePublic => ViewLookupCache;
        }
    }
}
