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

using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;

namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;

public class PageBinderFactoryTest
{
    private static readonly MvcOptions _options = new MvcOptions();
    private static readonly IOptions<MvcOptions> _optionsAccessor = Options.Create(_options);

    [Fact]
    public void GetModelBinderFactory_ReturnsNullIfPageHasNoBoundProperties()
    {
        // Arrange
        var actionDescriptor = new CompiledPageActionDescriptor
        {
            PageTypeInfo = typeof(PageWithNoBoundProperties).GetTypeInfo(),
        };
        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var binder = new ParameterBinder(
            modelMetadataProvider,
            modelBinderFactory,
            Mock.Of<IObjectModelValidator>(),
            _optionsAccessor,
            NullLoggerFactory.Instance);

        // Act
        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        // Assert
        Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
    }

    [Fact]
    public void GetModelBinderFactory_ReturnsNullIfPageModelHasNoBoundProperties()
    {
        // Arrange
        var actionDescriptor = new CompiledPageActionDescriptor
        {
            PageTypeInfo = typeof(PageWithNoBoundProperties).GetTypeInfo(),
            ModelTypeInfo = typeof(PageModelWithNoBoundProperties).GetTypeInfo(),
        };
        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var binder = new ParameterBinder(
            modelMetadataProvider,
            modelBinderFactory,
            Mock.Of<IObjectModelValidator>(),
            _optionsAccessor,
            NullLoggerFactory.Instance);

        // Act
        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        // Assert
        Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
    }

    [Fact]
    public void GetModelBinderFactory_ReturnsNullIfPageHasNoVisibleBoundProperties()
    {
        // Arrange
        var actionDescriptor = new CompiledPageActionDescriptor
        {
            PageTypeInfo = typeof(PageWithNoVisibleBoundProperties).GetTypeInfo(),
        };
        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var binder = new ParameterBinder(
            modelMetadataProvider,
            modelBinderFactory,
            Mock.Of<IObjectModelValidator>(),
            _optionsAccessor,
            NullLoggerFactory.Instance);

        // Act
        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        // Assert
        Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
    }

    [Fact]
    public void GetModelBinderFactory_ReturnsNullIfPageModelHasNoVisibleBoundProperties()
    {
        // Arrange
        var actionDescriptor = new CompiledPageActionDescriptor
        {
            PageTypeInfo = typeof(object).GetTypeInfo(),
            ModelTypeInfo = typeof(PageModelWithNoVisibleBoundProperties).GetTypeInfo(),
        };
        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var binder = new ParameterBinder(
            modelMetadataProvider,
            modelBinderFactory,
            Mock.Of<IObjectModelValidator>(),
            _optionsAccessor,
            NullLoggerFactory.Instance);

        // Act
        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        // Assert
        Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
    }

    [Fact]
    public void GetModelBinderFactory_ReturnsNullIfPageHasNoSettableBoundProperties()
    {
        // Arrange
        var actionDescriptor = new CompiledPageActionDescriptor
        {
            PageTypeInfo = typeof(PageWithReadOnlyProperties).GetTypeInfo(),
        };
        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var binder = new ParameterBinder(
            modelMetadataProvider,
            modelBinderFactory,
            Mock.Of<IObjectModelValidator>(),
            _optionsAccessor,
            NullLoggerFactory.Instance);

        // Act
        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        // Assert
        Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
    }

    [Fact]
    public void GetModelBinderFactory_ReturnsNullIfPageModelHasNoSettableBoundProperties()
    {
        // Arrange
        var actionDescriptor = new CompiledPageActionDescriptor
        {
            PageTypeInfo = typeof(PageWithReadOnlyProperties).GetTypeInfo(),
            ModelTypeInfo = typeof(PageModelWithReadOnlyProperties).GetTypeInfo(),
        };
        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var binder = new ParameterBinder(
            modelMetadataProvider,
            modelBinderFactory,
            Mock.Of<IObjectModelValidator>(),
            _optionsAccessor,
            NullLoggerFactory.Instance);

        // Act
        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        // Assert
        Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
    }

    [Fact]
    public async Task ModelBinderFactory_BindsPropertiesOnPage()
    {
        // Arrange
        var type = typeof(PageWithProperty).GetTypeInfo();

        var actionDescriptor = new CompiledPageActionDescriptor
        {
            BoundProperties = new[]
            {
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageWithProperty.Id),
                        ParameterType = typeof(int),
                        Property = type.GetProperty(nameof(PageWithProperty.Id)),
                    },
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageWithProperty.RouteDifferentValue),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageWithProperty.RouteDifferentValue)),
                    },
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageWithProperty.PropertyWithNoValue),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageWithProperty.PropertyWithNoValue)),
                    }
                },
            HandlerTypeInfo = type,
            PageTypeInfo = type,
        };

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var binder = new TestParameterBinder(new Dictionary<string, object>
            {
                { nameof(PageWithProperty.Id), 10 },
                { nameof(PageWithProperty.RouteDifferentValue), "route-value" }
            });

        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext(),
        };

        // Act
        await factory(page.PageContext, page);

        // Assert
        Assert.Equal(10, page.Id);
        Assert.Equal("route-value", page.RouteDifferentValue);
        Assert.Null(page.PropertyWithNoValue);
    }

    [Fact]
    public async Task ModelBinderFactory_BindsPropertiesOnPageModel()
    {
        // Arrange
        var type = typeof(PageModelWithProperty).GetTypeInfo();

        var actionDescriptor = new CompiledPageActionDescriptor
        {
            BoundProperties = new[]
            {
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithProperty.Id),
                        ParameterType = typeof(int),
                        Property = type.GetProperty(nameof(PageModelWithProperty.Id)),
                    },
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithProperty.RouteDifferentValue),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithProperty.RouteDifferentValue)),
                    },
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithProperty.PropertyWithNoValue),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithProperty.PropertyWithNoValue)),
                    }
                },

            HandlerTypeInfo = typeof(PageModelWithProperty).GetTypeInfo(),
            PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(),
            ModelTypeInfo = typeof(PageModelWithProperty).GetTypeInfo(),
        };

        var binder = new TestParameterBinder(new Dictionary<string, object>
            {
                { nameof(PageModelWithProperty.Id), 10 },
                { nameof(PageModelWithProperty.RouteDifferentValue), "route-value" }
            });

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext()
        };

        var model = new PageModelWithProperty();

        // Act
        await factory(page.PageContext, model);

        // Assert
        // Verify that the page properties were not bound.
        Assert.Equal(default, page.Id);
        Assert.Null(page.RouteDifferentValue);

        Assert.Equal(10, model.Id);
        Assert.Equal("route-value", model.RouteDifferentValue);
        Assert.Null(model.PropertyWithNoValue);
    }

    [Fact]
    public async Task ModelBinderFactory_PreservesExistingValueIfModelBindingFailed()
    {
        // Arrange
        var type = typeof(PageModelWithDefaultValue).GetTypeInfo();

        var actionDescriptor = new CompiledPageActionDescriptor
        {
            BoundProperties = new[]
            {
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithDefaultValue.PropertyWithDefaultValue),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithDefaultValue.PropertyWithDefaultValue)),
                    },
                },

            HandlerTypeInfo = type,
            PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(),
            ModelTypeInfo = type,
        };

        var binder = new TestParameterBinder(new Dictionary<string, object>());

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext()
        };

        var model = new PageModelWithDefaultValue();
        var defaultValue = model.PropertyWithDefaultValue;

        // Act
        await factory(page.PageContext, model);

        // Assert
        Assert.Equal(defaultValue, model.PropertyWithDefaultValue);
    }

    [Theory]
    [InlineData("Get")]
    [InlineData("GET")]
    [InlineData("gET")]
    public async Task ModelBinderFactory_BindsPropertyWithoutSupportsGet_WhenRequestIsGet(string method)
    {
        // Arrange
        var type = typeof(PageModelWithSupportsGetProperty).GetTypeInfo();

        var actionDescriptor = new CompiledPageActionDescriptor
        {
            BoundProperties = new[]
            {
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithSupportsGetProperty.SupportsGet),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)),
                        BindingInfo = new BindingInfo()
                        {
                            // Simulates placing a [BindProperty] on the property
                            RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute() { SupportsGet = true }).RequestPredicate,
                        }
                    },
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithSupportsGetProperty.Default),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.Default)),
                    },
                },

            HandlerTypeInfo = type,
            PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(),
            ModelTypeInfo = type,
        };

        var binder = new TestParameterBinder(new Dictionary<string, object>()
            {
                { "SupportsGet", "value" },
                { "Default", "set" },
            });

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext(method)
        };

        var model = new PageModelWithSupportsGetProperty();

        // Act
        await factory(page.PageContext, model);

        // Assert
        Assert.Equal("value", model.SupportsGet);
        Assert.Equal("set", model.Default);
    }

    [Fact]
    public async Task ModelBinderFactory_BindsPropertyWithoutSupportsGet_WhenRequestIsNotGet()
    {
        // Arrange
        var type = typeof(PageModelWithSupportsGetProperty).GetTypeInfo();

        var actionDescriptor = new CompiledPageActionDescriptor
        {
            BoundProperties = new[]
            {
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithSupportsGetProperty.SupportsGet),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)),
                        BindingInfo = new BindingInfo()
                        {
                            RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute() { SupportsGet = true }).RequestPredicate,
                        }
                    },
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithSupportsGetProperty.Default),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.Default)),
                    },
                },

            HandlerTypeInfo = type,
            PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(),
            ModelTypeInfo = type,
        };

        var binder = new TestParameterBinder(new Dictionary<string, object>()
            {
                { "SupportsGet", "value" },
                { "Default", "value" },
            });

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext()
        };

        page.HttpContext.Request.Method = "Post";

        var model = new PageModelWithSupportsGetProperty();

        // Act
        await factory(page.PageContext, model);

        // Assert
        Assert.Equal("value", model.SupportsGet);
        Assert.Equal("value", model.Default);
    }

    [Fact]
    public async Task CreatePropertyBinder_SkipsBindingPropertiesWithBindNever()
    {
        // Arrange
        var type = typeof(PageModelWithBindNeverProperty).GetTypeInfo();

        var actionDescriptor = new CompiledPageActionDescriptor
        {
            BoundProperties = new[]
            {
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithBindNeverProperty.BindNeverProperty),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithBindNeverProperty.BindNeverProperty)),
                    },
                },

            HandlerTypeInfo = type,
            PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(),
            ModelTypeInfo = type,
        };

        var binder = new TestParameterBinder(new Dictionary<string, object>
            {
                { nameof(PageModelWithBindNeverProperty.BindNeverProperty), "value" },
            });

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext()
        };

        var model = new PageModelWithBindNeverProperty();

        // Act
        await factory(page.PageContext, model);

        // Assert
        Assert.Null(model.BindNeverProperty);
    }

    [Fact]
    public async Task CreatePropertyBinder_ValidatesTopLevelProperties()
    {
        // Arrange
        var type = typeof(PageModelWithValidation).GetTypeInfo();

        var actionDescriptor = new CompiledPageActionDescriptor
        {
            BoundProperties = new[]
            {
                    new PageBoundPropertyDescriptor()
                    {
                        Name = nameof(PageModelWithValidation.Validated),
                        ParameterType = typeof(string),
                        Property = type.GetProperty(nameof(PageModelWithValidation.Validated)),
                    },
                },

            HandlerTypeInfo = type,
            PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(),
            ModelTypeInfo = type,
        };

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();

        var binder = new ParameterBinder(
            modelMetadataProvider,
            modelBinderFactory,
            new DefaultObjectValidator(
                modelMetadataProvider,
                new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                new MvcOptions()),
            _optionsAccessor,
            NullLoggerFactory.Instance);

        var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext()
        };

        var model = new PageModelWithValidation();

        // Act
        await factory(page.PageContext, model);

        // Assert
        var modelState = page.PageContext.ModelState;
        Assert.False(modelState.IsValid);
        Assert.Collection(
            modelState,
            kvp =>
            {
                Assert.Equal(nameof(PageModelWithValidation.Validated), kvp.Key);
            });
    }

    [Fact]
    public async Task CreateHandlerBinder_BindsHandlerParameters()
    {
        // Arrange
        var type = typeof(PageModelWithExecutors);
        var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnGet));

        // Act
        var parameterBinder = new TestParameterBinder(new Dictionary<string, object>()
            {
                { "id", "value" },
            });

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();
        var factory = PageBinderFactory.CreateHandlerBinder(
            parameterBinder,
            modelMetadataProvider,
            modelBinderFactory,
            actionDescriptor,
            actionDescriptor.HandlerMethods[0]);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext()
        };

        var model = new PageModelWithExecutors();
        var arguments = new Dictionary<string, object>();

        // Act
        await factory(page.PageContext, arguments);

        // Assert
        Assert.Collection(
            arguments,
            kvp =>
            {
                Assert.Equal("id", kvp.Key);
                Assert.Equal("value", kvp.Value);
            });
    }

    [Fact]
    public async Task CreateHandlerBinder_SkipBindingParametersThatDisallowBinding()
    {
        // Arrange
        var type = typeof(PageModelWithExecutors);
        var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnGetWithBindNever));

        // Act
        var parameterBinder = new TestParameterBinder(new Dictionary<string, object>()
            {
                { "id", "value" },
            });

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();
        var factory = PageBinderFactory.CreateHandlerBinder(
            parameterBinder,
            modelMetadataProvider,
            modelBinderFactory,
            actionDescriptor,
            actionDescriptor.HandlerMethods[0]);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext()
        };

        var model = new PageModelWithExecutors();
        var arguments = new Dictionary<string, object>();

        // Act
        await factory(page.PageContext, arguments);

        // Assert
        Assert.Empty(arguments);
    }

    [Fact]
    public async Task CreateHandlerBinder_ValidatesTopLevelParameters()
    {
        // Arrange
        var type = typeof(PageModelWithExecutors);
        var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnPostWithValidation));

        // Act

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();
        var parameterBinder = new ParameterBinder(
            modelMetadataProvider,
            modelBinderFactory,
            new DefaultObjectValidator(
                modelMetadataProvider,
                new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                new MvcOptions()),
            _optionsAccessor,
            NullLoggerFactory.Instance);

        var factory = PageBinderFactory.CreateHandlerBinder(
            parameterBinder,
            modelMetadataProvider,
            modelBinderFactory,
            actionDescriptor,
            actionDescriptor.HandlerMethods[0]);

        var page = new PageWithProperty
        {
            PageContext = GetPageContext()
        };

        var model = new PageModelWithExecutors();
        var arguments = new Dictionary<string, object>();

        // Act
        await factory(page.PageContext, arguments);

        // Assert
        var modelState = page.PageContext.ModelState;
        Assert.False(modelState.IsValid);
        Assert.Collection(
            modelState,
            kvp =>
            {
                Assert.Equal("name", kvp.Key);
            });
    }

    [Fact]
    public async Task FactoryRecordsErrorWhenValueProviderThrowsValueProviderException()
    {
        // Arrange
        var type = typeof(PageModelWithExecutors);
        var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnGet));

        // Act
        var parameterBinder = new TestParameterBinder(new Dictionary<string, object>()
            {
                { "id", "value" },
            });

        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelBinderFactory = TestModelBinderFactory.CreateDefault();
        var factory = PageBinderFactory.CreateHandlerBinder(
            parameterBinder,
            modelMetadataProvider,
            modelBinderFactory,
            actionDescriptor,
            actionDescriptor.HandlerMethods[0]);

        var pageContext = GetPageContext();
        var page = new PageWithProperty
        {
            PageContext = pageContext,
        };

        var valueProviderFactory = new Mock<IValueProviderFactory>();
        valueProviderFactory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>()))
            .Throws(new ValueProviderException("Some error"));

        pageContext.ValueProviderFactories.Add(valueProviderFactory.Object);

        var model = new PageModelWithExecutors();
        var arguments = new Dictionary<string, object>();

        // Act
        await factory(page.PageContext, arguments);

        // Assert
        var modelState = pageContext.ModelState;
        var entry = Assert.Single(modelState);
        Assert.Empty(entry.Key);
        var error = Assert.Single(entry.Value.Errors);

        Assert.Equal("Some error", error.ErrorMessage);
    }

    private static CompiledPageActionDescriptor GetActionDescriptorWithHandlerMethod(Type type, string method)
    {
        var handlerMethodInfo = type.GetMethod(method);
        var parameterInfo = handlerMethodInfo.GetParameters()[0];

        var actionDescriptor = new CompiledPageActionDescriptor
        {
            HandlerTypeInfo = type.GetTypeInfo(),
            HandlerMethods = new[]
            {
                    new HandlerMethodDescriptor
                    {
                        HttpMethod = "Post",
                        MethodInfo = handlerMethodInfo,
                        Parameters = new[]
                        {
                            new HandlerParameterDescriptor
                            {
                                ParameterInfo = parameterInfo,
                                ParameterType = parameterInfo.ParameterType,
                                Name = parameterInfo.Name
                            },
                        },
                    },
                },
        };
        return actionDescriptor;
    }

    private PageContext GetPageContext(string httpMethod = null)
    {
        var services = new ServiceCollection();
        services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);

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

        if (httpMethod != null)
        {
            httpContext.Request.Method = httpMethod;
        }

        return new PageContext()
        {
            HttpContext = httpContext
        };
    }

    private class TestParameterBinder : ParameterBinder
    {
        private readonly IDictionary<string, object> _args;

        public TestParameterBinder(IDictionary<string, object> args)
            : base(
                TestModelMetadataProvider.CreateDefaultProvider(),
                TestModelBinderFactory.CreateDefault(),
                Mock.Of<IObjectModelValidator>(),
                _optionsAccessor,
                NullLoggerFactory.Instance)
        {
            _args = args;
        }

        public IList<ParameterDescriptor> Descriptors { get; } = new List<ParameterDescriptor>();

        public override ValueTask<ModelBindingResult> BindModelAsync(
            ActionContext actionContext,
            IModelBinder modelBinder,
            IValueProvider valueProvider,
            ParameterDescriptor parameter,
            ModelMetadata metadata,
            object value,
            object container)
        {
            Descriptors.Add(parameter);

            if (_args.TryGetValue(parameter.Name, out var result))
            {
                return new ValueTask<ModelBindingResult>(ModelBindingResult.Success(result));
            }

            return new ValueTask<ModelBindingResult>(ModelBindingResult.Failed());
        }
    }

    private interface ITestService
    { }

    private class PageModelWithNoBoundProperties : PageModel
    {
    }

    private class PageWithNoBoundProperties : Page
    {
        public override Task ExecuteAsync() => Task.FromResult(0);
    }

    private class PageWithNoVisibleBoundProperties : Page
    {
        [FromBody]
        private string FromBody { get; set; }

        [FromQuery]
        protected string FromQuery { get; set; }

        [FromServices]
        protected ITestService FromService { get; set; }

        [FromRoute]
        public static int FromRoute { get; set; }

        public override Task ExecuteAsync() => Task.FromResult(0);
    }

    private class PageModelWithNoVisibleBoundProperties : PageModel
    {
        [FromBody]
        private string FromBody { get; set; }

        [FromQuery]
        protected string FromQuery { get; set; }

        [FromServices]
        protected ITestService FromService { get; set; }

        [FromRoute]
        public static int FromRoute { get; set; }
    }

    private class PageWithReadOnlyProperties : Page
    {
        [FromBody]
        private string FromBody { get; }

        public override Task ExecuteAsync() => Task.FromResult(0);
    }

    private class PageModelWithReadOnlyProperties
    {
        [FromBody]
        private string FromBody { get; }
    }

    private class PageWithProperty : Page
    {
        [FromQuery]
        public int Id { get; set; }

        [FromRoute(Name = "route-value")]
        public string RouteDifferentValue { get; set; }

        [FromForm]
        public string PropertyWithNoValue { get; set; }

        [FromServices]
        public ITestService FromService { get; set; }

        public override Task ExecuteAsync() => Task.FromResult(0);
    }

    private class PageModelWithProperty : PageModel
    {
        [FromQuery]
        public int Id { get; set; }

        [FromRoute(Name = "route-value")]
        public string RouteDifferentValue { get; set; }

        [FromForm]
        public string PropertyWithNoValue { get; set; }

        [FromServices]
        public ITestService FromService { get; set; }

    }

    private class PageModelWithDefaultValue
    {
        [ModelBinder]
        public string PropertyWithDefaultValue { get; set; } = "Hello world";
    }

    private class PageModelWithSupportsGetProperty
    {
        [BindProperty(SupportsGet = true)]
        public string SupportsGet { get; set; }

        public string Default { get; set; }
    }

    private class PageModelWithBindNeverProperty
    {
        [BindNever]
        public string BindNeverProperty { get; set; }
    }

    private class PageModelWithValidation
    {
        [Required]
        public string Validated { get; set; }
    }

    private class PageModelWithExecutors
    {
        public void OnGetWithBindNever([BindNever] string id)
        {
        }

        public void OnGet(string id)
        {
        }

        public void OnPostWithValidation([Required] string name)
        {
        }
    }
}
