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

using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Moq;

namespace Microsoft.AspNetCore.Mvc.Test;

public class ControllerTest
{
    public static IEnumerable<object[]> PublicNormalMethodsFromController
    {
        get
        {
            return typeof(Controller).GetTypeInfo()
                .DeclaredMethods
                .Where(method => method.IsPublic &&
                !method.IsSpecialName &&
                !method.Name.Equals("Dispose", StringComparison.OrdinalIgnoreCase))
                .Select(method => new[] { method });
        }
    }

    [Fact]
    public void SettingViewData_AlsoUpdatesViewBag()
    {
        // Arrange
        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var controller = new TestableController();
        var originalViewData = controller.ViewData = new ViewDataDictionary<object>(metadataProvider);
        var replacementViewData = new ViewDataDictionary<object>(metadataProvider);

        // Act
        controller.ViewBag.Hello = "goodbye";
        controller.ViewData = replacementViewData;
        controller.ViewBag.Another = "property";

        // Assert
        Assert.NotSame(originalViewData, controller.ViewData);
        Assert.Same(replacementViewData, controller.ViewData);
        Assert.Null(controller.ViewBag.Hello);
        Assert.Equal("property", controller.ViewBag.Another);
        Assert.Equal("property", controller.ViewData["Another"]);
    }

    [Theory]
    [MemberData(nameof(PublicNormalMethodsFromController))]
    public void NonActionAttribute_IsOnEveryPublicNormalMethodFromController(MethodInfo method)
    {
        // Arrange & Act & Assert
        Assert.True(method.IsDefined(typeof(NonActionAttribute)));
    }

    [Fact]
    public void Controller_View_WithoutParameter_SetsResultNullViewNameAndNullViewDataModelAndSameTempData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };

        // Act
        var actualViewResult = controller.View();

        // Assert
        Assert.IsType<ViewResult>(actualViewResult);
        Assert.Null(actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Null(actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_View_WithoutParameter_MaintainsModelInViewData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };

        var model = new object();
        controller.ViewData.Model = model;

        // Act
        var actualViewResult = controller.View();

        // Assert
        Assert.IsType<ViewResult>(actualViewResult);
        Assert.Null(actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Same(model, actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_View_WithParameterViewName_SetsResultViewNameAndNullViewDataModelAndSameTempData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };

        // Act
        var actualViewResult = controller.View("CustomViewName");

        // Assert
        Assert.IsType<ViewResult>(actualViewResult);
        Assert.Equal("CustomViewName", actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Null(actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_View_WithParameterViewModel_SetsResultNullViewNameAndViewDataModelAndSameTempData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };
        var model = new object();

        // Act
        var actualViewResult = controller.View(model);

        // Assert
        Assert.IsType<ViewResult>(actualViewResult);
        Assert.Null(actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Same(model, actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_View_WithParameterViewNameAndViewModel_SetsResultViewNameAndViewDataModelAndSameTempData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };
        var model = new object();

        // Act
        var actualViewResult = controller.View("CustomViewName", model);

        // Assert
        Assert.IsType<ViewResult>(actualViewResult);
        Assert.Equal("CustomViewName", actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Same(model, actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_View_WithNullModelParameter_OverwritesViewDataModel()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };
        controller.ViewData.Model = new object();

        // Act
        var actualViewResult = controller.View(model: null);

        // Assert
        Assert.IsType<ViewResult>(actualViewResult);
        Assert.Null(actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Null(actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_PartialView_WithoutParameter_MaintainsModelInViewData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };

        var model = new object();
        controller.ViewData.Model = model;

        // Act
        var actualViewResult = controller.PartialView();

        // Assert
        Assert.IsType<PartialViewResult>(actualViewResult);
        Assert.Null(actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Same(model, actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_PartialView_WithParameterViewName_SetsResultViewNameAndMaintainsSameViewDataModelAndTempData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };
        var model = new object();
        controller.ViewData.Model = model;

        // Act
        var actualViewResult = controller.PartialView("CustomViewName");

        // Assert
        Assert.IsType<PartialViewResult>(actualViewResult);
        Assert.Equal("CustomViewName", actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Same(model, actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_PartialView_WithParameterViewModel_SetsResultNullViewNameAndViewDataModelAndSameTempData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };
        var model = new object();

        // Act
        var actualViewResult = controller.PartialView(model);

        // Assert
        Assert.IsType<PartialViewResult>(actualViewResult);
        Assert.Null(actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Same(model, actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_PartialView_WithParameterViewNameAndViewModel_SetsResultViewNameAndViewDataModelAndSameTempData()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };
        var model = new object();

        // Act
        var actualViewResult = controller.PartialView("CustomViewName", model);

        // Assert
        Assert.IsType<PartialViewResult>(actualViewResult);
        Assert.Equal("CustomViewName", actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Same(model, actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_PartialView_WithNullModelParameter_OverwritesViewDataModel()
    {
        // Arrange
        var controller = new TestableController()
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
            TempData = new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>()),
        };
        controller.ViewData.Model = new object();

        // Act
        var actualViewResult = controller.PartialView(model: null);

        // Assert
        Assert.IsType<PartialViewResult>(actualViewResult);
        Assert.Null(actualViewResult.ViewName);
        Assert.Same(controller.ViewData, actualViewResult.ViewData);
        Assert.Same(controller.TempData, actualViewResult.TempData);
        Assert.Null(actualViewResult.ViewData.Model);
    }

    [Fact]
    public void Controller_Json_WithParameterValue_SetsResultData()
    {
        // Arrange
        var controller = new TestableController();
        var data = new object();

        // Act
        var actualJsonResult = controller.Json(data);

        // Assert
        Assert.IsType<JsonResult>(actualJsonResult);
        Assert.Same(data, actualJsonResult.Value);
    }

    [Fact]
    public void Controller_Json_WithParameterValue_SetsRespectiveProperty()
    {
        // Arrange
        var controller = new TestableController();
        var data = new object();
        var serializerSettings = new object();

        // Act
        var actualJsonResult = controller.Json(data, serializerSettings);

        // Assert
        Assert.IsType<JsonResult>(actualJsonResult);
        Assert.Same(data, actualJsonResult.Value);
        Assert.Same(serializerSettings, actualJsonResult.SerializerSettings);
    }

    // These tests share code with the ActionFilterAttribute tests because the various filter
    // implementations need to behave the same way.
    [Fact]
    public async Task Controller_ActionFilter_SettingResult_ShortCircuits()
    {
        // Arrange, Act &  Assert
        await CommonFilterTest.ActionFilter_SettingResult_ShortCircuits(
            new Mock<Controller>());
    }

    [Fact]
    public async Task Controller_ActionFilter_Calls_OnActionExecuted()
    {
        // Arrange, Act &  Assert
        await CommonFilterTest.ActionFilter_Calls_OnActionExecuted(
            new Mock<Controller>());
    }

    [Fact]
    public void ControllerDispose_CallsDispose()
    {
        // Arrange
        var controller = new DisposableController();

        // Act
        controller.Dispose();

        // Assert
        Assert.True(controller.DisposeCalled);
    }

    [Fact]
    public void TempData_CanSetAndGetValues()
    {
        // Arrange
        var controller = GetController(null, null);
        var input = "Foo";

        // Act
        controller.TempData["key"] = input;
        var result = controller.TempData["key"];

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

    public static TheoryData<object, Type> IncompatibleModelData
    {
        get
        {
            // Small grab bag of instances and expected types with no common base except typeof(object).
            return new TheoryData<object, Type>
                {
                    { null, typeof(object) },
                    { true, typeof(bool) },
                    { 43.78, typeof(double) },
                    { "test string", typeof(string) },
                    { new List<int>(), typeof(List<int>) },
                    { new List<string>(), typeof(List<string>) },
                };
        }
    }

    [Theory]
    [MemberData(nameof(IncompatibleModelData))]
    public void ViewDataModelSetter_DoesNotThrow(object model, Type expectedType)
    {
        // Arrange
        var activator = new ViewDataDictionaryControllerPropertyActivator(new EmptyModelMetadataProvider());
        var actionContext = new ActionContext(
            new DefaultHttpContext(),
            new RouteData(),
            new ControllerActionDescriptor());
        var controllerContext = new ControllerContext(actionContext);
        var controller = new TestableController();
        activator.Activate(controllerContext, controller);

        // Guard
        Assert.NotNull(controller.ViewData);

        // Act (does not throw)
        controller.ViewData.Model = model;

        // Assert
        Assert.NotNull(controller.ViewData.ModelMetadata);
        Assert.Equal(expectedType, controller.ViewData.ModelMetadata.ModelType);
    }

    private static Controller GetController(IModelBinder binder, IValueProvider valueProvider)
    {
        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var httpContext = new DefaultHttpContext();

        var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
        var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());

        var validatorProviders = new[]
        {
                new DataAnnotationsModelValidatorProvider(
                    new ValidationAttributeAdapterProvider(),
                    Options.Create(new MvcDataAnnotationsLocalizationOptions()),
                    stringLocalizerFactory: null),
            };

        valueProvider = valueProvider ?? new SimpleValueProvider();
        var controllerContext = new ControllerContext()
        {
            HttpContext = httpContext,
            ValueProviderFactories = new[] { new SimpleValueProviderFactory(valueProvider), },
        };

        var controller = new TestableController()
        {
            ControllerContext = controllerContext,
            MetadataProvider = metadataProvider,
            ObjectValidator = new DefaultObjectValidator(metadataProvider, validatorProviders, new MvcOptions()),
            TempData = tempData,
            ViewData = viewData,
        };
        return controller;
    }

    private class DisposableController : Controller
    {
        public bool DisposeCalled { get; private set; }

        protected override void Dispose(bool disposing)
        {
            DisposeCalled = true;
        }
    }

    private class TestableController : Controller
    {
    }

    private class DisposableObject : IDisposable
    {
        public void Dispose()
        {
            throw new NotImplementedException();
        }
    }
}
