// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.WebEncoders.Testing;
using Microsoft.Net.Http.Headers;
using Xunit;

namespace Microsoft.AspNetCore.Mvc
{
    public class ViewComponentResultTest
    {
        private readonly ITempDataDictionary _tempDataDictionary =
            new TempDataDictionary(new DefaultHttpContext(), new SessionStateTempDataProvider());

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

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

        [Fact]
        public async Task ExecuteAsync_ViewComponentResult_AllowsNullViewDataAndTempData()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var viewComponentResult = new ViewComponentResult
            {
                Arguments = new { name = "World!" },
                ViewData = null,
                TempData = null,
                ViewComponentName = "Text"
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);
            // No assert, just confirm it didn't throw
        }

        [Fact]
        public async Task ExecuteResultAsync_Throws_IfNameOrTypeIsNotSet()
        {
            // Arrange
            var expected =
                "Either the 'ViewComponentName' or 'ViewComponentType' " +
                "property must be set in order to invoke a view component.";

            var actionContext = CreateActionContext();

            var viewComponentResult = new ViewComponentResult
            {
                TempData = _tempDataDictionary,
            };

            // Act and Assert
            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
                () => viewComponentResult.ExecuteResultAsync(actionContext));
            Assert.Equal(expected, exception.Message);
        }

        [Fact]
        public async Task ExecuteResultAsync_Throws_IfViewComponentCouldNotBeFound_ByName()
        {
            // Arrange
            var expected = "A view component named 'Text' could not be found. A view component must be " +
                "a public non-abstract class, not contain any generic parameters, and either be decorated " +
                "with 'ViewComponentAttribute' or have a class name ending with the 'ViewComponent' suffix. " +
                "A view component must not be decorated with 'NonViewComponentAttribute'.";

            var actionContext = CreateActionContext();

            var viewComponentResult = new ViewComponentResult
            {
                ViewComponentName = "Text",
                TempData = _tempDataDictionary,
            };

            // Act and Assert
            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
                () => viewComponentResult.ExecuteResultAsync(actionContext));
            Assert.Equal(expected, exception.Message);
        }

        [Fact]
        public async Task ExecuteResultAsync_Throws_IfViewComponentCouldNotBeFound_ByType()
        {
            // Arrange
            var expected = $"A view component named '{typeof(TextViewComponent).FullName}' could not be found. " +
                "A view component must be a public non-abstract class, not contain any generic parameters, and either be decorated " +
                "with 'ViewComponentAttribute' or have a class name ending with the 'ViewComponent' suffix. " +
                "A view component must not be decorated with 'NonViewComponentAttribute'.";

            var actionContext = CreateActionContext();
            var services = CreateServices(diagnosticListener: null, context: actionContext.HttpContext);
            services.AddSingleton<IViewComponentSelector>();


            var viewComponentResult = new ViewComponentResult
            {
                ViewComponentType = typeof(TextViewComponent),
                TempData = _tempDataDictionary,
            };

            // Act and Assert
            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
                () => viewComponentResult.ExecuteResultAsync(actionContext));
            Assert.Equal(expected, exception.Message);
        }

        [Fact]
        public async Task ExecuteResultAsync_ExecutesSyncViewComponent()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentName = "Text",
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            var body = ReadBody(actionContext.HttpContext.Response);
            Assert.Equal("Hello, World!", body);
        }

        [Fact]
        public async Task ExecuteResultAsync_UsesDictionaryArguments()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new Dictionary<string, object> { ["name"] = "World!" },
                ViewComponentName = "Text",
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            var body = ReadBody(actionContext.HttpContext.Response);
            Assert.Equal("Hello, World!", body);
        }

        [Fact]
        public async Task ExecuteResultAsync_ExecutesAsyncViewComponent()
        {
            // Arrange
            var methodInfo = typeof(AsyncTextViewComponent).GetMethod(nameof(AsyncTextViewComponent.InvokeAsync));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.AsyncText",
                ShortName = "AsyncText",
                TypeInfo = typeof(AsyncTextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentName = "AsyncText",
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            var body = ReadBody(actionContext.HttpContext.Response);
            Assert.Equal("Hello-Async, World!", body);
        }

        [Fact]
        public async Task ExecuteResultAsync_ExecutesViewComponent_AndWritesDiagnosticSource()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var adapter = new TestDiagnosticListener();

            var actionContext = CreateActionContext(adapter, descriptor);

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentName = "Text",
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            var body = ReadBody(actionContext.HttpContext.Response);
            Assert.Equal("Hello, World!", body);

            Assert.NotNull(adapter.BeforeViewComponent?.ActionDescriptor);
            Assert.NotNull(adapter.BeforeViewComponent?.ViewComponentContext);
            Assert.NotNull(adapter.BeforeViewComponent?.ViewComponent);
            Assert.NotNull(adapter.AfterViewComponent?.ActionDescriptor);
            Assert.NotNull(adapter.AfterViewComponent?.ViewComponentContext);
            Assert.NotNull(adapter.AfterViewComponent?.ViewComponentResult);
            Assert.NotNull(adapter.AfterViewComponent?.ViewComponent);
        }

        [Fact]
        public async Task ExecuteResultAsync_ExecutesViewComponent_ByShortName()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentName = "Text",
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            var body = ReadBody(actionContext.HttpContext.Response);
            Assert.Equal("Hello, World!", body);
        }

        [Fact]
        public async Task ExecuteResultAsync_ExecutesViewComponent_ByFullName()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentName = "Full.Name.Text",
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            var body = ReadBody(actionContext.HttpContext.Response);
            Assert.Equal("Hello, World!", body);
        }

        [Fact]
        public async Task ExecuteResultAsync_ExecutesViewComponent_ByType()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentType = typeof(TextViewComponent),
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            var body = ReadBody(actionContext.HttpContext.Response);
            Assert.Equal("Hello, World!", body);
        }

        [Fact]
        public async Task ExecuteResultAsync_SetsStatusCode()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentType = typeof(TextViewComponent),
                StatusCode = 404,
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            Assert.Equal(404, actionContext.HttpContext.Response.StatusCode);
        }

        public static TheoryData<string, string> ViewComponentResultContentTypeData
        {
            get
            {
                return new TheoryData<string, string>
                {
                    {
                        null,
                        "text/html; charset=utf-8"
                    },
                    {
                        "text/foo",
                        "text/foo"
                    },
                    {
                        "text/foo;p1=p1-value",
                        "text/foo; p1=p1-value"
                    },
                    {
                        new MediaTypeHeaderValue("text/foo") { Encoding = Encoding.ASCII }.ToString(),
                        "text/foo; charset=us-ascii"
                    }
                };
            }
        }

        [Theory]
        [MemberData(nameof(ViewComponentResultContentTypeData))]
        public async Task ViewComponentResult_SetsContentTypeHeader(
            string contentType,
            string expectedContentType)
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var contentTypeBeforeViewResultExecution = contentType?.ToString();

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentName = "Text",
                ContentType = contentType,
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            var resultContentType = actionContext.HttpContext.Response.ContentType;
            MediaTypeAssert.Equal(expectedContentType, resultContentType);

            // Check if the original instance provided by the user has not changed.
            // Since we do not have access to the new instance created within the view executor,
            // check if at least the content is the same.
            var contentTypeAfterViewResultExecution = contentType?.ToString();
            MediaTypeAssert.Equal(contentTypeBeforeViewResultExecution, contentTypeAfterViewResultExecution);
        }

        [Fact]
        public async Task ViewComponentResult_SetsContentTypeHeader_OverrideResponseContentType()
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            var expectedContentType = "text/html; charset=utf-8";
            actionContext.HttpContext.Response.ContentType = "application/x-will-be-overridden";

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentName = "Text",
                ContentType = new MediaTypeHeaderValue("text/html") { Encoding = Encoding.UTF8 }.ToString(),
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            Assert.Equal(expectedContentType, actionContext.HttpContext.Response.ContentType);
        }

        [Theory]
        [InlineData("text/foo", "text/foo; charset=utf-8")]
        [InlineData("text/foo; p1=p1-value", "text/foo; p1=p1-value; charset=utf-8")]
        [InlineData("text/foo; p1=p1-value; charset=us-ascii", "text/foo; p1=p1-value; charset=us-ascii")]
        public async Task ViewComponentResult_NoContentTypeSet_PreservesResponseContentType(
            string responseContentType,
            string expectedContentType)
        {
            // Arrange
            var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
            var descriptor = new ViewComponentDescriptor()
            {
                FullName = "Full.Name.Text",
                ShortName = "Text",
                TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
                MethodInfo = methodInfo,
                Parameters = methodInfo.GetParameters(),
            };

            var actionContext = CreateActionContext(descriptor);

            actionContext.HttpContext.Response.ContentType = expectedContentType;

            var viewComponentResult = new ViewComponentResult()
            {
                Arguments = new { name = "World!" },
                ViewComponentName = "Text",
                TempData = _tempDataDictionary,
            };

            // Act
            await viewComponentResult.ExecuteResultAsync(actionContext);

            // Assert
            Assert.Equal(expectedContentType, actionContext.HttpContext.Response.ContentType);
        }

        private IServiceCollection CreateServices(
            object diagnosticListener,
            HttpContext context,
            params ViewComponentDescriptor[] descriptors)
        {
            var httpContext = new DefaultHttpContext();
            var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
            if (diagnosticListener != null)
            {
                diagnosticSource.SubscribeWithAdapter(diagnosticListener);
            }

            var services = new ServiceCollection();
            services.AddSingleton<DiagnosticSource>(diagnosticSource);
            services.AddSingleton<ViewComponentInvokerCache>();
            services.AddSingleton<ExpressionTextCache>();
            services.AddSingleton<IOptions<MvcViewOptions>, TestOptionsManager<MvcViewOptions>>();
            services.AddTransient<IViewComponentHelper, DefaultViewComponentHelper>();
            services.AddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
            services.AddSingleton<IViewComponentDescriptorCollectionProvider, DefaultViewComponentDescriptorCollectionProvider>();
            services.AddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
            services.AddSingleton<ITypeActivatorCache, TypeActivatorCache>();
            services.AddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
            services.AddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
            services.AddSingleton<IViewComponentDescriptorProvider>(new FixedSetViewComponentDescriptorProvider(descriptors));
            services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
            services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
            services.AddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
            services.AddSingleton<ITempDataProvider, SessionStateTempDataProvider>();
            services.AddSingleton<HtmlEncoder, HtmlTestEncoder>();
            services.AddSingleton<IViewBufferScope, TestViewBufferScope>();
            services.AddSingleton<ViewComponentResultExecutor>();

            return services;
        }

        private HttpContext CreateHttpContext(object diagnosticListener, params ViewComponentDescriptor[] descriptors)
        {
            var httpContext = new DefaultHttpContext();
            var services = CreateServices(diagnosticListener, httpContext, descriptors);

            httpContext.Response.Body = new MemoryStream();
            httpContext.RequestServices = services.BuildServiceProvider();

            return httpContext;
        }

        private ActionContext CreateActionContext(object diagnosticListener, params ViewComponentDescriptor[] descriptors)
        {
            return new ActionContext(CreateHttpContext(diagnosticListener, descriptors), new RouteData(), new ActionDescriptor());
        }

        private ActionContext CreateActionContext(params ViewComponentDescriptor[] descriptors)
        {
            return CreateActionContext(null, descriptors);
        }


        private class FixedSetViewComponentDescriptorProvider : IViewComponentDescriptorProvider
        {
            private readonly ViewComponentDescriptor[] _descriptors;

            public FixedSetViewComponentDescriptorProvider(params ViewComponentDescriptor[] descriptors)
            {
                _descriptors = descriptors ?? new ViewComponentDescriptor[0];
            }

            public IEnumerable<ViewComponentDescriptor> GetViewComponents()
            {
                return _descriptors;
            }
        }

        private static string ReadBody(HttpResponse response)
        {
            response.Body.Seek(0, SeekOrigin.Begin);

            using (var reader = new StreamReader(response.Body))
            {
                return reader.ReadToEnd();
            }
        }

        private class TextViewComponent : ViewComponent
        {
            public HtmlString Invoke(string name)
            {
                return new HtmlString("Hello, " + name);
            }
        }

        private class AsyncTextViewComponent : ViewComponent
        {
            public Task<HtmlString> InvokeAsync(string name)
            {
                return Task.FromResult(new HtmlString("Hello-Async, " + name));
            }
        }
    }
}
