// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using Microsoft.AspNetCore.Components.Endpoints.FormMapping;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Routing.Internal;

public partial class RequestDelegateFactoryTests : LoggedTest
{
    [Fact]
    public async Task SupportsFormMappingOptionsInMetadata()
    {
        // Arrange
        static void TestAction([FromForm] Dictionary<string, string> args) { }
        var options = new RequestDelegateFactoryOptions
        {
            EndpointBuilder = CreateEndpointBuilder(new List<object>()
            {
                new FormMappingOptionsMetadata(maxCollectionSize: 2)
            }),
            ThrowOnBadRequest = true
        };
        var metadataResult = new RequestDelegateMetadataResult { EndpointMetadata = new List<object>() };
        var httpContext = CreateHttpContext();
        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
        {
            {
                "[name1]", "value1"
            },
            {
                "[name2]", "value2"
            },
            {
                "[name3]", "value3"
            },
            {
                "[name4]", "value4"
            },
            {
                "[name5]", "value5"
            },
            {
                "[name6]", "value6"
            }
        });

        var factoryResult = RequestDelegateFactory.Create(TestAction, options, metadataResult);
        var requestDelegate = factoryResult.RequestDelegate;

        // Act
        var exception = await Assert.ThrowsAsync<BadHttpRequestException>(async () => await requestDelegate(httpContext));

        // Assert
        Assert.Equal("The number of elements in the dictionary exceeded the maximum number of '2' elements allowed.", exception.Message);
    }

    [Fact]
    public async Task SupportsFormMappingOptionsInMetadataFormFormWithAttributeName()
    {
        // Arrange
        static void TestAction([FromForm(Name = "shouldSetKeyCorrectly")] Dictionary<string, string> args) { }
        var options = new RequestDelegateFactoryOptions
        {
            EndpointBuilder = CreateEndpointBuilder(new List<object>()
            {
                new FormMappingOptionsMetadata(maxCollectionSize: 2)
            }),
            ThrowOnBadRequest = true
        };
        var metadataResult = new RequestDelegateMetadataResult { EndpointMetadata = new List<object>() };
        var httpContext = CreateHttpContext();
        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
        {
            {
                "[name1]", "value1"
            },
            {
                "[name2]", "value2"
            },
            {
                "[name3]", "value3"
            },
            {
                "[name4]", "value4"
            },
            {
                "[name5]", "value5"
            },
            {
                "[name6]", "value6"
            }
        });

        var factoryResult = RequestDelegateFactory.Create(TestAction, options, metadataResult);
        var requestDelegate = factoryResult.RequestDelegate;

        // Act
        var exception = await Assert.ThrowsAsync<BadHttpRequestException>(async () => await requestDelegate(httpContext));

        // Assert
        Assert.Equal("The number of elements in the dictionary exceeded the maximum number of '2' elements allowed.", exception.Message);
    }

    [Fact]
    public async Task SupportsMergingFormMappingOptionsInMetadata()
    {
        // Arrange
        static void TestAction([FromForm] Dictionary<string, string> args) { }
        var options = new RequestDelegateFactoryOptions
        {
            EndpointBuilder = CreateEndpointBuilder(new List<object>()
            {
                new FormMappingOptionsMetadata(maxCollectionSize: 2),
                new FormMappingOptionsMetadata(maxKeySize: 23)
            }),
            ThrowOnBadRequest = true
        };
        var metadataResult = new RequestDelegateMetadataResult { EndpointMetadata = new List<object>() };
        var httpContext = CreateHttpContext();
        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
        {
            {
                "[name1]", "value1"
            },
            {
                "[name2]", "value2"
            },
            {
                "[name3]", "value3"
            },
            {
                "[name4]", "value4"
            },
            {
                "[name5]", "value5"
            },
            {
                "[name6]", "value6"
            }
        });

        var factoryResult = RequestDelegateFactory.Create(TestAction, options, metadataResult);
        var requestDelegate = factoryResult.RequestDelegate;

        // Act
        var exception = await Assert.ThrowsAsync<BadHttpRequestException>(async () => await requestDelegate(httpContext));

        // Assert
        Assert.Equal("The number of elements in the dictionary exceeded the maximum number of '2' elements allowed.", exception.Message);

        // Arrange - 2
        httpContext = CreateHttpContext();
        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
        {
            {
                "[name1name1name1name1name1name1name1]", "value1"
            },
            {
                "[name2name2name2name2name2name2name2]", "value2"
            },
        });

        // Act - 2
        var anotherException = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await requestDelegate(httpContext));

        // Assert - 2
        Assert.Equal("Specified argument was out of the range of valid values.", anotherException.Message);
    }

    [Fact]
    public async Task SupportsFormMappingWithRecordTypes()
    {
        TodoRecord capturedTodo = default;
        void TestAction([FromForm] TodoRecord args) { capturedTodo = args; };
        var httpContext = CreateHttpContext();
        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
        {
            {
                "id", "1"
            },
            {
                "name", "Write tests"
            },
            {
                "isCompleted", "false"
            }
        });

        var factoryResult = RequestDelegateFactory.Create(TestAction);
        var requestDelegate = factoryResult.RequestDelegate;

        await requestDelegate(httpContext);

        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
        Assert.Equal(1, capturedTodo.Id);
        Assert.Equal("Write tests", capturedTodo.Name);
        Assert.False(capturedTodo.IsCompleted);
    }

    [Fact]
    public async Task SupportsRecursiveProperties()
    {
        Employee capturedEmployee = default;
        void TestAction([FromForm] Employee args) { capturedEmployee = args; };
        var httpContext = CreateHttpContext();
        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
        {
            {
                "Name", "A"
            },
            {
                "Manager.Name", "B"
            },
            {
                "Manager.Manager.Name", "C"
            },
            {
                "Manager.Manager.Manager.Name", "D"
            }
        });

        var factoryResult = RequestDelegateFactory.Create(TestAction);
        var requestDelegate = factoryResult.RequestDelegate;

        await requestDelegate(httpContext);

        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
    }

    [Fact]
    public async Task SupportsRecursivePropertiesWithRecursionLimit()
    {
        Employee capturedEmployee = default;
        var options = new RequestDelegateFactoryOptions
        {
            EndpointBuilder = CreateEndpointBuilder(new List<object>()
            {
                new FormMappingOptionsMetadata(maxRecursionDepth: 3)
            }),
            ThrowOnBadRequest = true
        };
        var metadataResult = new RequestDelegateMetadataResult { EndpointMetadata = new List<object>() };
        void TestAction([FromForm] Employee args) { capturedEmployee = args; };
        var httpContext = CreateHttpContext();
        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
        {
            {
                "Name", "A"
            },
            {
                "Manager.Name", "B"
            },
            {
                "Manager.Manager.Name", "C"
            },
            {
                "Manager.Manager.Manager.Name", "D"
            },
            {
                "Manager.Manager.Manager.Manager.Name", "E"
            },
            {
                "Manager.Manager.Manager.Manager.Manager.Name", "F"
            },
            {
                "Manager.Manager.Manager.Manager.Manager.Manager.Name", "G"
            }
        });

        var factoryResult = RequestDelegateFactory.Create(TestAction, options, metadataResult);
        var requestDelegate = factoryResult.RequestDelegate;

        var exception = await Assert.ThrowsAsync<BadHttpRequestException>(async () => await requestDelegate(httpContext));

        Assert.Equal("The maximum recursion depth of '3' was exceeded for 'Manager.Manager.Manager.Name'.", exception.Message);
    }

    [Fact]
    public async Task SupportsFormFileSourcesInDto()
    {
        FormFileDto capturedArgument = default;
        void TestAction([FromForm] FormFileDto args) { capturedArgument = args; };
        var httpContext = CreateHttpContext();
        var formFiles = new FormFileCollection
        {
            new FormFile(Stream.Null, 0, 10, "file", "file.txt"),
            new FormFile(Stream.Null, 0, 10, "formFiles", "file-1.txt"),
            new FormFile(Stream.Null, 0, 10, "formFiles", "file-2.txt"),
        };
        httpContext.Request.Form = new FormCollection(new() { { "Description", "A test file" } }, formFiles);

        var factoryResult = RequestDelegateFactory.Create(TestAction);
        var requestDelegate = factoryResult.RequestDelegate;

        await requestDelegate(httpContext);
        Assert.Equal("A test file", capturedArgument.Description);
        Assert.Equal(formFiles["file"], capturedArgument.File);
        Assert.Equal(formFiles.GetFiles("formFiles"), capturedArgument.FormFiles);
        Assert.Equal(formFiles, capturedArgument.FormFileCollection);
    }

    private record TodoRecord(int Id, string Name, bool IsCompleted);

    private class Employee
    {
        public string Name { get; set; }
        public Employee Manager { get; set; }
    }
#nullable enable

    private class FormFileDto
    {
        public string Description { get; set; } = String.Empty;
        public IFormFile? File { get; set; }
        public IReadOnlyList<IFormFile>? FormFiles { get; set; }
        public IFormFileCollection? FormFileCollection { get; set; }
    }
}
