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

using System.Diagnostics;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;

namespace Microsoft.AspNetCore.Mvc.Razor;

public class RazorPageActivatorTest
{
    public RazorPageActivatorTest()
    {
        DiagnosticListener = new DiagnosticListener("Microsoft.AspNetCore");
        HtmlEncoder = new HtmlTestEncoder();
        JsonHelper = Mock.Of<IJsonHelper>();
        MetadataProvider = new EmptyModelMetadataProvider();
        ModelExpressionProvider = new ModelExpressionProvider(MetadataProvider);
        UrlHelperFactory = new UrlHelperFactory();
    }

    private DiagnosticListener DiagnosticListener { get; }

    private HtmlEncoder HtmlEncoder { get; }

    private IJsonHelper JsonHelper { get; }

    private IModelMetadataProvider MetadataProvider { get; }

    private IModelExpressionProvider ModelExpressionProvider { get; }

    private IUrlHelperFactory UrlHelperFactory { get; }

    [Fact]
    public void Activate_ContextualizesServices_AndSetsProperties_OnPage()
    {
        // Arrange
        var activator = CreateActivator();

        var instance = new TestRazorPage();
        var viewData = new ViewDataDictionary<MyModel>(MetadataProvider, new ModelStateDictionary());
        var viewContext = CreateViewContext();

        var urlHelper = UrlHelperFactory.GetUrlHelper(viewContext);

        // Act
        activator.Activate(instance, viewContext);

        // Assert
        Assert.Same(DiagnosticListener, instance.DiagnosticSource);
        Assert.Same(HtmlEncoder, instance.HtmlEncoder);
        Assert.Same(JsonHelper, instance.Json);
        Assert.Same(urlHelper, instance.Url);
        Assert.Same(viewContext.ViewData, instance.ViewData);

        // Has no [RazorInject] so it shouldn't get injected
        Assert.Null(instance.MyService2);

        // We're not testing the IViewContextualizable implementation here because it's a mock.
        Assert.NotNull(instance.Html);
        Assert.IsAssignableFrom<IHtmlHelper<object>>(instance.Html);

        var service = instance.MyService;
        var keyedService = instance.MyKeyedService;
        Assert.NotNull(service);
        Assert.NotNull(keyedService);
        Assert.Same(viewContext, service.ViewContext);
        Assert.Same(viewContext, keyedService.ViewContext);
    }

    [Fact]
    public void Activate_ContextualizesServices_AndSetsProperties_OnPageWithoutModel()
    {
        // Arrange
        var activator = CreateActivator();

        var viewData = new ViewDataDictionary<object>(MetadataProvider, new ModelStateDictionary());
        var viewContext = CreateViewContext(viewData);

        var urlHelper = UrlHelperFactory.GetUrlHelper(viewContext);

        var instance = new NoModelPropertyPage();

        // Act
        activator.Activate(instance, viewContext);

        // Assert
        Assert.Same(DiagnosticListener, instance.DiagnosticSource);
        Assert.Same(HtmlEncoder, instance.HtmlEncoder);

        // When we don't have a model property, the activator will just leave ViewData alone.
        Assert.NotNull(viewContext.ViewData);
    }

    [Fact]
    public void Activate_InstantiatesNewViewDataDictionaryType_IfTheTypeDoesNotMatch()
    {
        // Arrange
        var activator = CreateActivator();

        var viewData = new ViewDataDictionary<object>(MetadataProvider, new ModelStateDictionary())
            {
                { "key", "value" },
            };
        var viewContext = CreateViewContext(viewData);

        var urlHelper = UrlHelperFactory.GetUrlHelper(viewContext);

        var instance = new TestRazorPage();

        // Act
        activator.Activate(instance, viewContext);

        // Assert
        Assert.Same(DiagnosticListener, instance.DiagnosticSource);
        Assert.Same(HtmlEncoder, instance.HtmlEncoder);
        Assert.Same(JsonHelper, instance.Json);
        Assert.Same(urlHelper, instance.Url);
        Assert.Same(viewContext.ViewData, instance.ViewData);

        // The original ViewDataDictionary was replaced.
        Assert.NotSame(viewData, viewContext.ViewData);
        Assert.NotSame(viewData, instance.ViewData);

        // But this value is copied
        Assert.Equal("value", viewData["key"]);

        // Has no [RazorInject] so it shouldn't get injected
        Assert.Null(instance.MyService2);

        // We're not testing the IViewContextualizable implementation here because it's a mock.
        Assert.NotNull(instance.Html);
        Assert.IsAssignableFrom<IHtmlHelper<object>>(instance.Html);

        var service = instance.MyService;
        var keyedService = instance.MyKeyedService;
        Assert.NotNull(service);
        Assert.NotNull(keyedService);
        Assert.Same(viewContext, service.ViewContext);
        Assert.Same(viewContext, keyedService.ViewContext);
    }

    [Fact]
    public void Activate_Throws_WhenViewDataPropertyHasIncorrectType()
    {
        // Arrange
        var activator = CreateActivator();

        var viewData = new ViewDataDictionary<MyModel>(MetadataProvider, new ModelStateDictionary());
        var viewContext = CreateViewContext(viewData);

        var instance = new HasIncorrectViewDataPropertyType();

        // Act & Assert
        Assert.Throws<InvalidCastException>(() => activator.Activate(instance, viewContext));
    }

    [Fact]
    public void Activate_UsesModelFromModelTypeProvider()
    {
        // Arrange
        var activator = CreateActivator();

        var viewData = new ViewDataDictionary<object>(MetadataProvider, new ModelStateDictionary())
            {
                { "key", "value" },
            };
        var viewContext = CreateViewContext(viewData);
        var page = new ModelTypeProviderRazorPage();

        // Act
        activator.Activate(page, viewContext);

        // Assert
        Assert.Same(viewContext.ViewData, page.ViewData);
        Assert.NotSame(viewData, viewContext.ViewData);

        Assert.IsType<ViewDataDictionary<Guid>>(viewContext.ViewData);
        Assert.Equal("value", viewContext.ViewData["key"]);
    }

    [Fact]
    public void GetOrAddCacheEntry_CachesPages()
    {
        // Arrange
        var activator = CreateActivator();
        var page = new TestRazorPage();

        // Act
        var result1 = activator.GetOrAddCacheEntry(page);
        var result2 = activator.GetOrAddCacheEntry(page);

        // Assert
        Assert.Same(result1, result2);
    }

    [Fact]
    public void GetOrAddCacheEntry_VariesByModelType_IfPageIsModelTypeProvider()
    {
        // Arrange
        var activator = CreateActivator();
        var page = new ModelTypeProviderRazorPage();

        // Act - 1
        var result1 = activator.GetOrAddCacheEntry(page);
        var result2 = activator.GetOrAddCacheEntry(page);

        // Assert - 1
        Assert.Same(result1, result2);

        // Act - 2
        page.ModelType = typeof(string);
        var result3 = activator.GetOrAddCacheEntry(page);
        var result4 = activator.GetOrAddCacheEntry(page);

        // Assert - 2
        Assert.Same(result3, result4);
        Assert.NotSame(result1, result3);
    }

    private RazorPageActivator CreateActivator()
    {
        return new RazorPageActivator(MetadataProvider, UrlHelperFactory, JsonHelper, DiagnosticListener, HtmlEncoder, ModelExpressionProvider);
    }

    private const string KeyedServiceKey = "my-keyed-service";

    private ViewContext CreateViewContext(ViewDataDictionary viewData = null)
    {
        if (viewData == null)
        {
            viewData = new ViewDataDictionary(MetadataProvider, new ModelStateDictionary());
        }

        var myService = new MyService();
        var htmlHelper = Mock.Of<IHtmlHelper<object>>();

        var serviceProvider = new ServiceCollection()
            .AddSingleton(myService)
            .AddSingleton(htmlHelper)
            .AddKeyedSingleton(KeyedServiceKey, myService)
            .BuildServiceProvider();

        var httpContext = new DefaultHttpContext
        {
            RequestServices = serviceProvider
        };
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
        return new ViewContext(
            actionContext,
            Mock.Of<IView>(),
            viewData,
            Mock.Of<ITempDataDictionary>(),
            TextWriter.Null,
            new HtmlHelperOptions());
    }

    private abstract class TestPageBase<TModel> : RazorPage<TModel>
    {
        [RazorInject]
        public MyService MyService { get; set; }

        public MyService MyService2 { get; set; }

        [RazorInject]
        public IJsonHelper Json { get; set; }

        [RazorInject]
        public IUrlHelper Url { get; set; }

        [RazorInject(Key = KeyedServiceKey)]
        public MyService MyKeyedService { get; set; }
    }

    private class TestRazorPage : TestPageBase<MyModel>
    {
        [RazorInject]
        internal IHtmlHelper<object> Html { get; private set; }

        public override Task ExecuteAsync()
        {
            throw new NotImplementedException();
        }
    }

    private class ModelTypeProviderRazorPage : RazorPage, IModelTypeProvider
    {
        [RazorInject]
        public ViewDataDictionary ViewData { get; set; }

        public Type ModelType { get; set; } = typeof(Guid);

        public override Task ExecuteAsync()
        {
            throw new NotImplementedException();
        }

        public Type GetModelType() => ModelType;
    }

    private abstract class NoModelPropertyBase<TModel> : RazorPage
    {
        [RazorInject]
        public ViewDataDictionary ViewData { get; set; }
    }

    private class NoModelPropertyPage : NoModelPropertyBase<MyModel>
    {
        public override Task ExecuteAsync()
        {
            throw new NotImplementedException();
        }
    }

    private class HasIncorrectViewDataPropertyType : RazorPage<MyModel>
    {
        [RazorInject]
        public ViewDataDictionary<object> MoreViewData { get; set; }

        public override Task ExecuteAsync()
        {
            throw new NotImplementedException();
        }
    }

    private class MyService : IViewContextAware
    {
        public ViewContext ViewContext { get; private set; }

        public void Contextualize(ViewContext viewContext)
        {
            ViewContext = viewContext;
        }
    }

    private class MyModel
    {
    }
}
