// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc
{
    // These tests cover the logic included in ViewResult.ExecuteResultAsync - see ViewResultExecutorTest
    // and ViewExecutorTest for more comprehensive tests.
    public class ViewResultTest
    {
        [Fact]
        public void Model_NullWhenViewDataIsNull()
        {
            // Arrange
            var viewResult = new ViewResult();

            // Act & Assert
            Assert.Null(viewResult.Model);
        }

        [Fact]
        public void Model_ExposesViewDataModel()
        {
            // Arrange
            var customModel = new object();
            var viewResult = new ViewResult
            {
                ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider())
                {
                    Model = customModel
                },
            };

            // Act & Assert
            Assert.Same(customModel, viewResult.Model);
        }

        [Fact]
        public async Task ExecuteResultAsync_Throws_IfServicesNotRegistered()
        {
            // Arrange
            var actionContext = new ActionContext(new DefaultHttpContext() { RequestServices = Mock.Of<IServiceProvider>(), }, new RouteData(), new ActionDescriptor());
            var expected =
                $"Unable to find the required services. Please add all the required services by calling " +
                $"'IServiceCollection.AddControllersWithViews()' inside the call to 'ConfigureServices(...)' " +
                $"in the application startup code.";

            var viewResult = new ViewResult();

            // Act
            var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => viewResult.ExecuteResultAsync(actionContext));

            // Assert
            Assert.Equal(expected, ex.Message);
        }

        [Fact]
        public async Task ExecuteResultAsync_Throws_IfViewCouldNotBeFound_MessageUsesGetViewLocations()
        {
            // Arrange
            var viewName = "MyView";
            var actionContext = GetActionContext();
            var expected = string.Join(
                Environment.NewLine,
                $"The view '{viewName}' was not found. The following locations were searched:",
                "Location1",
                "Location2");

            var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, viewName, /*isMainPage*/ true))
                .Returns(ViewEngineResult.NotFound(viewName, Enumerable.Empty<string>()))
                .Verifiable();

            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), viewName, /*isMainPage*/ true))
                .Returns(ViewEngineResult.NotFound(viewName, new[] { "Location1", "Location2" }))
                .Verifiable();

            var viewResult = new ViewResult
            {
                ViewEngine = viewEngine.Object,
                ViewName = viewName,
                ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
                TempData = Mock.Of<ITempDataDictionary>(),
            };

            // Act and Assert
            var ex = await Assert.ThrowsAsync<InvalidOperationException>(
                () => viewResult.ExecuteResultAsync(actionContext));
            Assert.Equal(expected, ex.Message);
            viewEngine.Verify();
        }

        [Fact]
        public async Task ExecuteResultAsync_Throws_IfViewCouldNotBeFound_MessageUsesFindViewLocations()
        {
            // Arrange
            var viewName = "MyView";
            var actionContext = GetActionContext();
            var expected = string.Join(
                Environment.NewLine,
                $"The view '{viewName}' was not found. The following locations were searched:",
                "Location1",
                "Location2",
                "Location3",
                "Location4");

            var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, viewName, /*isMainPage*/ true))
                .Returns(ViewEngineResult.NotFound(viewName, new[] { "Location1", "Location2" }))
                .Verifiable();

            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), viewName, /*isMainPage*/ true))
                .Returns(ViewEngineResult.NotFound(viewName, new[] { "Location3", "Location4" }))
                .Verifiable();

            var viewResult = new ViewResult
            {
                ViewEngine = viewEngine.Object,
                ViewName = viewName,
                ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
                TempData = Mock.Of<ITempDataDictionary>(),
            };

            // Act and Assert
            var ex = await Assert.ThrowsAsync<InvalidOperationException>(
                () => viewResult.ExecuteResultAsync(actionContext));
            Assert.Equal(expected, ex.Message);
            viewEngine.Verify();
        }

        [Fact]
        public async Task ExecuteResultAsync_Throws_IfViewCouldNotBeFound_MessageUsesAllLocations()
        {
            // Arrange
            var viewName = "MyView";
            var actionContext = GetActionContext();
            var expected = string.Join(
                Environment.NewLine,
                $"The view '{viewName}' was not found. The following locations were searched:",
                "Location1",
                "Location2",
                "Location3",
                "Location4");

            var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, viewName, /*isMainPage*/ true))
                .Returns(ViewEngineResult.NotFound(viewName, new[] { "Location1", "Location2" }))
                .Verifiable();

            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), viewName, /*isMainPage*/ true))
                .Returns(ViewEngineResult.NotFound(viewName, new[] { "Location3", "Location4" }))
                .Verifiable();

            var viewResult = new ViewResult
            {
                ViewEngine = viewEngine.Object,
                ViewName = viewName,
                ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
                TempData = Mock.Of<ITempDataDictionary>(),
            };

            // Act and Assert
            var ex = await Assert.ThrowsAsync<InvalidOperationException>(
                () => viewResult.ExecuteResultAsync(actionContext));
            Assert.Equal(expected, ex.Message);
            viewEngine.Verify();
        }

        [Fact]
        public async Task ExecuteResultAsync_FindsAndExecutesView()
        {
            // Arrange
            var viewName = "MyView";
            var actionContext = GetActionContext();

            var view = new Mock<IView>(MockBehavior.Strict);
            view
                .Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
                .Returns(Task.FromResult(0))
                .Verifiable();

            view
                .As<IDisposable>()
                .Setup(v => v.Dispose())
                .Verifiable();

            // Used by logging
            view
                .SetupGet(v => v.Path)
                .Returns($"{viewName}.cshtml");

            var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, viewName, /*isMainPage*/ true))
                .Returns(ViewEngineResult.NotFound(viewName, Enumerable.Empty<string>()))
                .Verifiable();

            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), viewName, /*isMainPage*/ true))
                .Returns(ViewEngineResult.Found(viewName, view.Object))
                .Verifiable();

            var viewResult = new ViewResult
            {
                ViewName = viewName,
                ViewEngine = viewEngine.Object,
                ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
                TempData = Mock.Of<ITempDataDictionary>(),
            };

            // Act
            await viewResult.ExecuteResultAsync(actionContext);

            // Assert
            view.Verify();
            viewEngine.Verify();
        }

        private ActionContext GetActionContext()
        {
            return new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
        }

        private HttpContext GetHttpContext()
        {
            var options = Options.Create(new MvcViewOptions());

            var viewExecutor = new ViewResultExecutor(
                options,
                new TestHttpResponseStreamWriterFactory(),
                new CompositeViewEngine(options),
                Mock.Of<ITempDataDictionaryFactory>(),
                new DiagnosticListener("Microsoft.AspNetCore"),
                NullLoggerFactory.Instance,
                new EmptyModelMetadataProvider());

            var services = new ServiceCollection();
            services.AddSingleton<IActionResultExecutor<ViewResult>>(viewExecutor);

            var httpContext = new DefaultHttpContext();
            httpContext.RequestServices = services.BuildServiceProvider();
            return httpContext;
        }
    }
}
