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

using System.Buffers;
using System.Linq.Expressions;
using System.Reflection;
using System.Security.Claims;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.OpenApi.Tests;

public class OpenApiOperationGeneratorTests
{
    [Fact]
    public void OperationNotCreatedIfNoHttpMethods()
    {
        var operation = GetOpenApiOperation(() => { }, "/", Array.Empty<string>());

        Assert.Null(operation);
    }

    [Fact]
    public void UsesDeclaringTypeAsOperationTags()
    {
        var operation = GetOpenApiOperation(TestAction);
        var declaringTypeName = typeof(OpenApiOperationGeneratorTests).Name;

        var tag = Assert.Single(operation.Tags);

        Assert.Equal(declaringTypeName, tag.Name);

    }

    [Fact]
    public void UsesApplicationNameAsOperationTagsIfNoDeclaringType()
    {
        var operation = GetOpenApiOperation(() => { });

        var declaringTypeName = nameof(OpenApiOperationGeneratorTests);
        var tag = Assert.Single(operation.Tags);

        Assert.Equal(declaringTypeName, tag.Name);
    }

    [Fact]
    public void UsesTagsFromMultipleCallsToWithTags()
    {
        var testBuilder = new TestEndpointConventionBuilder();
        var routeHandlerBuilder = new RouteHandlerBuilder(new[] { testBuilder });

        routeHandlerBuilder
            .WithTags("A")
            .WithTags("B");

        var operation = GetOpenApiOperation(() => { }, additionalMetadata: testBuilder.Metadata.ToArray());

        Assert.Collection(operation.Tags,
            tag => Assert.Equal("A", tag.Name),
            tag => Assert.Equal("B", tag.Name));
    }

    [Fact]
    public void ThrowsInvalidOperationExceptionGivenUnnamedParameter()
    {
        var unnamedParameter = Expression.Parameter(typeof(int));
        var lambda = Expression.Lambda(Expression.Block(), unnamedParameter);
        var ex = Assert.Throws<InvalidOperationException>(() => GetOpenApiOperation(lambda.Compile()));
        Assert.Equal("Encountered a parameter of type 'System.Runtime.CompilerServices.Closure' without a name. Parameters must have a name.", ex.Message);
    }

    [Fact]
    public void AddsRequestFormatFromMetadata()
    {
        static void AssertCustomRequestFormat(OpenApiOperation operation)
        {
            Assert.NotNull(operation.Parameters);
            Assert.Empty(operation.Parameters);
            var content = operation.RequestBody.Content!.Keys.FirstOrDefault();
            Assert.Equal("application/custom", content);
        }

        AssertCustomRequestFormat(GetOpenApiOperation(
            [Consumes("application/custom")] (InferredJsonClass fromBody) => { }));

        AssertCustomRequestFormat(GetOpenApiOperation(
            [Consumes("application/custom")] ([FromBody] int fromBody) => { }));
    }

    [Fact]
    public void AddsMultipleRequestFormatsFromMetadata()
    {
        var operation = GetOpenApiOperation(
            [Consumes("application/custom0", "application/custom1")] (InferredJsonClass fromBody) => { });

        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);

        var content = operation.RequestBody.Content!;
        Assert.Equal(2, content.Count);
        Assert.Equal(["application/custom0", "application/custom1"], content.Keys);
    }

    [Fact]
    public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBodyParameter()
    {
        var operation = GetOpenApiOperation(
            [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = true)] () => { });
        var request = operation.RequestBody;
        Assert.NotNull(request);
        Assert.Equal(2, request.Content.Count);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);

        Assert.Equal("application/custom0", request.Content.First().Key);
        Assert.Equal("application/custom1", request.Content.Last().Key);
        Assert.False(request.Required);
    }

#nullable enable

    [Fact]
    public void AddsMultipleRequestFormatsFromMetadataWithRequiredBodyParameter()
    {
        var operation = GetOpenApiOperation(
            [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = false)] (InferredJsonClass fromBody) => { });

        var request = operation.RequestBody;
        Assert.NotNull(request);

        Assert.Equal("application/custom0", request.Content!.First().Key);
        Assert.Equal("application/custom1", request.Content!.Last().Key);
        Assert.True(request.Required);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

#nullable disable

    [Fact]
    public void AddsJsonResponseFormatWhenFromBodyInferred()
    {
        static void AssertJsonResponse(OpenApiOperation operation, JsonSchemaType expectedType)
        {
            var response = Assert.Single(operation.Responses);
            Assert.Equal("200", response.Key);
            var formats = Assert.Single(response.Value.Content);

            Assert.Equal("application/json", formats.Key);
        }

        AssertJsonResponse(GetOpenApiOperation(() => new InferredJsonClass()), JsonSchemaType.Object);
        AssertJsonResponse(GetOpenApiOperation(() => (IInferredJsonInterface)null), JsonSchemaType.Object);
        AssertJsonResponse(GetOpenApiOperation(() => Task.FromResult(new InferredJsonClass())), JsonSchemaType.Object);
        AssertJsonResponse(GetOpenApiOperation(() => Task.FromResult((IInferredJsonInterface)null)), JsonSchemaType.Object);
        AssertJsonResponse(GetOpenApiOperation(() => ValueTask.FromResult(new InferredJsonClass())), JsonSchemaType.Object);
        AssertJsonResponse(GetOpenApiOperation(() => ValueTask.FromResult((IInferredJsonInterface)null)), JsonSchemaType.Object);
        AssertJsonResponse(GetOpenApiOperation(() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new InferredJsonClass())), JsonSchemaType.Object);
        AssertJsonResponse(GetOpenApiOperation(() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return((IInferredJsonInterface)null)), JsonSchemaType.Object);
    }

    [Fact]
    public void AddsTextResponseFormatWhenFromBodyInferred()
    {
        var operation = GetOpenApiOperation(() => "foo");

        var response = Assert.Single(operation.Responses);
        Assert.Equal("200", response.Key);
        var formats = Assert.Single(response.Value.Content);
        Assert.Equal("text/plain", formats.Key);
    }

    [Fact]
    public void AddsNoResponseFormatWhenItCannotBeInferredAndTheresNoMetadata()
    {
        static void AssertVoid(OpenApiOperation operation)
        {
            var response = Assert.Single(operation.Responses);
            Assert.Equal("200", response.Key);
            Assert.Empty(response.Value.Content);
        }

        AssertVoid(GetOpenApiOperation(() => { }));
        AssertVoid(GetOpenApiOperation(() => Task.CompletedTask));
        AssertVoid(GetOpenApiOperation(() => Task.FromResult(default(FSharp.Core.Unit))));
        AssertVoid(GetOpenApiOperation(() => new ValueTask()));
        AssertVoid(GetOpenApiOperation(() => ValueTask.FromResult(default(FSharp.Core.Unit))));
        AssertVoid(GetOpenApiOperation(() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(FSharp.Core.Unit))));
    }

    [Fact]
    public void AddsMultipleResponseFormatsFromMetadataWithPoco()
    {
        var operation = GetOpenApiOperation(
            [ProducesResponseType(typeof(TimeSpan), StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        () => new InferredJsonClass());

        var responses = operation.Responses;

        Assert.Equal(2, responses.Count);

        var createdResponseType = responses["201"];
        var content = Assert.Single(createdResponseType.Content);

        Assert.NotNull(createdResponseType);
        Assert.Equal("application/json", content.Key);

        var badRequestResponseType = responses["400"];

        Assert.NotNull(badRequestResponseType);
        var badRequestContent = Assert.Single(badRequestResponseType.Content);
        Assert.Equal("application/json", badRequestContent.Key);
    }

    [Fact]
    public void AddsMultipleResponseFormatsFromMetadataWithIResult()
    {
        var operation = GetOpenApiOperation(
            [ProducesResponseType(typeof(InferredJsonClass), StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        () => Results.Ok(new InferredJsonClass()));

        Assert.Equal(2, operation.Responses.Count);

        var createdResponseType = operation.Responses["201"];
        var createdResponseContent = Assert.Single(createdResponseType.Content);

        Assert.NotNull(createdResponseType);
        Assert.Equal("application/json", createdResponseContent.Key);

        var badRequestResponseType = operation.Responses["400"];

        Assert.NotNull(badRequestResponseType);
        Assert.Empty(badRequestResponseType.Content);
    }

    [Fact]
    public void DefaultResponseDescriptionIsCorrect()
    {
        var operation = GetOpenApiOperation(
        [ProducesResponseType(typeof(TimeSpan), StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        () => new InferredJsonClass());

        Assert.Equal(2, operation.Responses.Count);

        var successResponse = operation.Responses["201"];
        Assert.Equal("Created", successResponse.Description);

        var clientErrorResponse = operation.Responses["400"];
        Assert.Equal("Bad Request", clientErrorResponse.Description);
    }

    [Fact]
    public void DefaultResponseDescriptionIsCorrectForTwoSimilarResponses()
    {
        var operation = GetOpenApiOperation(
        [ProducesResponseType(StatusCodes.Status100Continue)]
        [ProducesResponseType(StatusCodes.Status101SwitchingProtocols)]
        () => new InferredJsonClass());

        Assert.Equal(2, operation.Responses.Count);

        var continueResponse = operation.Responses["100"];
        Assert.Equal("Continue", continueResponse.Description);

        var switchingProtocolsResponse = operation.Responses["101"];
        Assert.Equal("Switching Protocols", switchingProtocolsResponse.Description);
    }

    [Fact]
    public void AllDefaultResponseDescriptions()
    {
        var operation = GetOpenApiOperation(
        [ProducesResponseType(StatusCodes.Status100Continue)]
        [ProducesResponseType(typeof(TimeSpan), StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status300MultipleChoices)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status500InternalServerError)]
        () => new InferredJsonClass());

        Assert.Equal(5, operation.Responses.Count);

        var continueResponse = operation.Responses["100"];
        Assert.Equal("Continue", continueResponse.Description);

        var createdResponse = operation.Responses["201"];
        Assert.Equal("Created", createdResponse.Description);

        var multipleChoicesResponse = operation.Responses["300"];
        Assert.Equal("Multiple Choices", multipleChoicesResponse.Description);

        var badRequestResponse = operation.Responses["400"];
        Assert.Equal("Bad Request", badRequestResponse.Description);

        var InternalServerErrorResponse = operation.Responses["500"];
        Assert.Equal("Internal Server Error", InternalServerErrorResponse.Description);
    }

    [Fact]
    public void UnregisteredStatusCodeDescriptions()
    {
        var operation = GetOpenApiOperation(
        [ProducesResponseType(46)]
        [ProducesResponseType(654)]
        [ProducesResponseType(1111)]
        () => new InferredJsonClass());

        Assert.Equal(3, operation.Responses.Count);

        var unregisteredResponse1 = operation.Responses["46"];
        Assert.Equal("", unregisteredResponse1.Description);

        var unregisteredResponse2 = operation.Responses["654"];
        Assert.Equal("", unregisteredResponse2.Description);

        var unregisteredResponse3 = operation.Responses["1111"];
        Assert.Equal("", unregisteredResponse3.Description);
    }

    [Fact]
    public void AddsFromRouteParameterAsPath()
    {
        static void AssertPathParameter(OpenApiOperation operation)
        {
            var param = Assert.Single(operation.Parameters);
            Assert.Equal(ParameterLocation.Path, param.In);
            Assert.Null(param.Content);
        }

        AssertPathParameter(GetOpenApiOperation((int foo) => { }, "/{foo}"));
        AssertPathParameter(GetOpenApiOperation(([FromRoute] int foo) => { }));
    }

    [Fact]
    public void AddsFromRouteParameterAsPathWithCustomClassWithTryParse()
    {
        static void AssertPathParameter(OpenApiOperation operation)
        {
            var param = Assert.Single(operation.Parameters);
            Assert.Equal(ParameterLocation.Path, param.In);
            Assert.Null(param.Content);
        }
        AssertPathParameter(GetOpenApiOperation((TryParseStringRecord foo) => { }, pattern: "/{foo}"));
    }

    [Fact]
    public void AddsFromRouteParameterAsPathWithNullablePrimitiveType()
    {
        static void AssertPathParameter(OpenApiOperation operation)
        {
            var param = Assert.Single(operation.Parameters);
            Assert.Equal(ParameterLocation.Path, param.In);
            Assert.Null(param.Content);
        }

        AssertPathParameter(GetOpenApiOperation((int? foo) => { }, "/{foo}"));
        AssertPathParameter(GetOpenApiOperation(([FromRoute] int? foo) => { }));
    }

    [Fact]
    public void AddsFromRouteParameterAsPathWithStructTypeWithTryParse()
    {
        static void AssertPathParameter(OpenApiOperation operation)
        {
            var param = Assert.Single(operation.Parameters);
            Assert.Equal(ParameterLocation.Path, param.In);
            Assert.Null(param.Content);
        }
        AssertPathParameter(GetOpenApiOperation((TryParseStringRecordStruct foo) => { }, pattern: "/{foo}"));
    }

    [Fact]
    public void AddsFromQueryParameterAsQuery()
    {
        static void AssertQueryParameter(OpenApiOperation operation, JsonSchemaType type)
        {
            var param = Assert.Single(operation.Parameters);
            Assert.Equal(ParameterLocation.Query, param.In);
            Assert.Null(param.Content);
        }

        AssertQueryParameter(GetOpenApiOperation((int foo) => { }, "/"), JsonSchemaType.Integer);
        AssertQueryParameter(GetOpenApiOperation(([FromQuery] int foo) => { }), JsonSchemaType.Integer);
        AssertQueryParameter(GetOpenApiOperation(([FromQuery] TryParseStringRecordStruct foo) => { }), JsonSchemaType.Object);
        AssertQueryParameter(GetOpenApiOperation((int[] foo) => { }, "/"), JsonSchemaType.Array);
        AssertQueryParameter(GetOpenApiOperation((string[] foo) => { }, "/"), JsonSchemaType.Array);
        AssertQueryParameter(GetOpenApiOperation((StringValues foo) => { }, "/"), JsonSchemaType.Array);
        AssertQueryParameter(GetOpenApiOperation((TryParseStringRecordStruct[] foo) => { }, "/"), JsonSchemaType.Array);
    }

    [Fact]
    public void AddsFromHeaderParameterAsHeader()
    {
        var operation = GetOpenApiOperation(([FromHeader] int foo) => { });
        var param = Assert.Single(operation.Parameters);

        Assert.Equal(ParameterLocation.Header, param.In);
        Assert.Null(param.Content);
    }

    [Fact]
    public void DoesNotAddFromServiceParameterAsService()
    {
        Assert.Empty(GetOpenApiOperation((IInferredServiceInterface foo) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation(([FromServices] int foo) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation(([CustomFromServices] int foo) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation(([FromKeyedServices("foo")] int foo) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation(([CustomFromKeyedServices("foo")] int foo) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation((HttpContext context) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation((HttpRequest request) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation((HttpResponse response) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation((ClaimsPrincipal user) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation((CancellationToken token) => { }).Parameters);
        Assert.Empty(GetOpenApiOperation((BindAsyncRecord context) => { }).Parameters);
    }

    [Fact]
    public void AddsBodyParameterInTheParameterDescription()
    {
        static void AssertBodyParameter(OpenApiOperation operation, string expectedName, JsonSchemaType expectedType)
        {
            var requestBody = operation.RequestBody;
            Assert.NotNull(requestBody);
            var content = Assert.Single(requestBody.Content!);
            Assert.Equal("application/json", content.Key);
            Assert.NotNull(operation.Parameters);
            Assert.Empty(operation.Parameters);
        }

        AssertBodyParameter(GetOpenApiOperation((InferredJsonClass foo) => { }), "foo", JsonSchemaType.Object);
        AssertBodyParameter(GetOpenApiOperation(([FromBody] int bar) => { }), "bar", JsonSchemaType.Integer);
    }

#nullable enable

    [Fact]
    public void AddsMultipleParameters()
    {
        var operation = GetOpenApiOperation(([FromRoute] int foo, int bar, InferredJsonClass fromBody) => { });
        Assert.NotNull(operation.Parameters);
        Assert.Equal(2, operation.Parameters.Count);

        var fooParam = operation.Parameters[0];
        Assert.Equal("foo", fooParam.Name);
        Assert.Equal(ParameterLocation.Path, fooParam.In);
        Assert.True(fooParam.Required);
        Assert.Null(fooParam.Content);

        var barParam = operation.Parameters[1];
        Assert.Equal("bar", barParam.Name);
        Assert.Equal(ParameterLocation.Query, barParam.In);
        Assert.True(barParam.Required);
        Assert.Null(barParam.Content);

        var fromBodyParam = operation.RequestBody;
        Assert.NotNull(fromBodyParam);
        var fromBodyContent = Assert.Single(fromBodyParam.Content!);
        Assert.Equal("application/json", fromBodyContent.Key);
        Assert.True(fromBodyParam.Required);
    }
#nullable disable

    [Fact]
    public void AddsMultipleParametersFromParametersAttribute()
    {
        static void AssertParameters(OpenApiOperation operation, string capturedName = "Foo")
        {
            Assert.Collection(
                operation.Parameters,
                param =>
                {
                    Assert.Equal(capturedName, param.Name);
                    Assert.Equal(ParameterLocation.Path, param.In);
                    Assert.True(param.Required);
                    Assert.Null(param.Content);
                },
                param =>
                {
                    Assert.Equal("Bar", param.Name);
                    Assert.Equal(ParameterLocation.Query, param.In);
                    Assert.True(param.Required);
                    Assert.Null(param.Content);
                }
            );
        }

        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListClass req) => { }));
        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListClassWithReadOnlyProperties req) => { }));
        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListStruct req) => { }));
        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecord req) => { }));
        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecordStruct req) => { }));
        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecordWithoutPositionalParameters req) => { }));
        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecordWithoutAttributes req) => { }, "/{foo}"), "foo");
        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecordWithoutAttributes req) => { }, "/{Foo}"));
    }

    [Fact]
    public void TestParameterIsRequired()
    {
        var operation = GetOpenApiOperation(([FromRoute] int foo, int? bar) => { });
        Assert.Equal(2, operation.Parameters.Count);

        var fooParam = operation.Parameters[0];
        Assert.Equal("foo", fooParam.Name);
        Assert.Equal(ParameterLocation.Path, fooParam.In);
        Assert.True(fooParam.Required);
        Assert.Null(fooParam.Content);

        var barParam = operation.Parameters[1];
        Assert.Equal("bar", barParam.Name);
        Assert.Equal(ParameterLocation.Query, barParam.In);
        Assert.False(barParam.Required);
        Assert.Null(barParam.Content);
    }

    [Fact]
    public void TestParameterIsRequiredForObliviousNullabilityContext()
    {
        // In an oblivious nullability context, reference type parameters without
        // annotations are optional. Value type parameters are always required.
        var operation = GetOpenApiOperation((string foo, int bar) => { });
        Assert.Equal(2, operation.Parameters.Count);

        var fooParam = operation.Parameters[0];
        Assert.Equal(ParameterLocation.Query, fooParam.In);
        Assert.False(fooParam.Required);

        var barParam = operation.Parameters[1];
        Assert.Equal(ParameterLocation.Query, barParam.In);
        Assert.True(barParam.Required);
    }

    [Fact]
    public void RespectProducesProblemMetadata()
    {
        // Arrange
        var operation = GetOpenApiOperation(() => "",
            additionalMetadata: new[] {
                new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(ProblemDetails), new [] { "application/json+problem" })
            });

        // Assert
        var responses = Assert.Single(operation.Responses);
        var content = Assert.Single(responses.Value.Content);
        Assert.Equal("application/json+problem", content.Key);
    }

    [Fact]
    public void RespectsProducesWithGroupNameExtensionMethod()
    {
        // Arrange
        var endpointGroupName = "SomeEndpointGroupName";
        var operation = GetOpenApiOperation(() => "",
            additionalMetadata: new object[]
            {
                new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(InferredJsonClass), new[] { "application/json" }),
                new EndpointNameMetadata(endpointGroupName)
            });

        var responses = Assert.Single(operation.Responses);
        var content = Assert.Single(responses.Value.Content);
        Assert.Equal("application/json", content.Key);
    }

    [Fact]
    public void RespectsExcludeFromDescription()
    {
        // Arrange
        var operation = GetOpenApiOperation(() => "",
            additionalMetadata: new object[]
            {
                new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(InferredJsonClass), new[] { "application/json" }),
                new ExcludeFromDescriptionAttribute()
            });

        Assert.Null(operation);
    }

    [Fact]
    public void HandlesProducesWithProducesProblem()
    {
        // Arrange
        var operation = GetOpenApiOperation(() => "",
            additionalMetadata: new[]
            {
                    new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(InferredJsonClass), new[] { "application/json" }),
                    new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(HttpValidationProblemDetails), new[] { "application/problem+json" }),
                    new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(ProblemDetails), new[] { "application/problem+json" }),
                    new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(ProblemDetails), new[] { "application/problem+json" })
            });
        var responses = operation.Responses;

        // Assert
        Assert.Collection(
            responses.OrderBy(response => response.Key),
            responseType =>
            {
                var content = Assert.Single(responseType.Value.Content);
                Assert.Equal("200", responseType.Key);
                Assert.Equal("application/json", content.Key);
            },
            responseType =>
            {
                var content = Assert.Single(responseType.Value.Content);
                Assert.Equal("400", responseType.Key);
                Assert.Equal("application/problem+json", content.Key);
            },
            responseType =>
            {
                var content = Assert.Single(responseType.Value.Content);
                Assert.Equal("404", responseType.Key);
                Assert.Equal("application/problem+json", content.Key);
            },
            responseType =>
            {
                var content = Assert.Single(responseType.Value.Content);
                Assert.Equal("409", responseType.Key);
                Assert.Equal("application/problem+json", content.Key);
            });
    }

    [Fact]
    public void HandleMultipleProduces()
    {
        // Arrange
        var operation = GetOpenApiOperation(() => "",
            additionalMetadata: new[]
            {
                new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(InferredJsonClass), new[] { "application/json" }),
                new ProducesResponseTypeMetadata(StatusCodes.Status201Created, typeof(InferredJsonClass), new[] { "application/json" })
            });

        var responses = operation.Responses;

        // Assert
        Assert.Collection(
            responses.OrderBy(response => response.Key),
            responseType =>
            {
                var content = Assert.Single(responseType.Value.Content);
                Assert.Equal("200", responseType.Key);
                Assert.Equal("application/json", content.Key);
            },
            responseType =>
            {
                var content = Assert.Single(responseType.Value.Content);
                Assert.Equal("201", responseType.Key);
                Assert.Equal("application/json", content.Key);
            });
    }

    [Fact]
    public void HandleAcceptsMetadataWithNoParams()
    {
        // Arrange
        var operation = GetOpenApiOperation(() => "",
            additionalMetadata: new[]
            {
                new AcceptsMetadata(new string[] { "application/json", "application/xml"}, typeof(string), true)
            });

        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);

        // Assert
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
        Assert.Collection(
            requestBody.Content!,
            parameter =>
            {
                Assert.Equal("application/json", parameter.Key);
            },
            parameter =>
            {
                Assert.Equal("application/xml", parameter.Key);
            });
    }

    [Fact]
    public void HandleAcceptsMetadataWithTypeParameter()
    {
        // Arrange
        var operation = GetOpenApiOperation((InferredJsonClass inferredJsonClass) => "",
                additionalMetadata: new[]
                {
                    new AcceptsMetadata(new string[] { "application/json" }, typeof(InferredJsonClass), true)
                });

        // Assert
        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);
        var content = Assert.Single(requestBody.Content!);
        Assert.False(requestBody.Required);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

#nullable enable

    [Fact]
    public void HandleDefaultIAcceptsMetadataForRequiredBodyParameter()
    {
        // Arrange
        var operation = GetOpenApiOperation((InferredJsonClass inferredJsonClass) => "");

        // Assert
        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);
        var content = Assert.Single(requestBody.Content!);
        Assert.Equal("application/json", content.Key);
        Assert.True(requestBody.Required);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

    [Fact]
    public void HandleDefaultIAcceptsMetadataForOptionalBodyParameter()
    {
        // Arrange
        var operation = GetOpenApiOperation((InferredJsonClass? inferredJsonClass) => "");

        // Assert
        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);
        var content = Assert.Single(requestBody.Content!);
        Assert.Equal("application/json", content.Key);
        Assert.False(requestBody.Required);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

    [Fact]
    public void HandleIAcceptsMetadataWithConsumesAttributeAndInferredOptionalFromBodyType()
    {
        // Arrange
        var operation = GetOpenApiOperation([Consumes("application/xml")] (InferredJsonClass? inferredJsonClass) => "");

        // Assert
        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);
        var content = Assert.Single(requestBody.Content!);
        Assert.Equal("application/xml", content.Key);
        Assert.False(requestBody.Required);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

    [Fact]
    public void HandleDefaultIAcceptsMetadataForRequiredFormFileParameter()
    {
        // Arrange
        var operation = GetOpenApiOperation((IFormFile inferredFormFile) => "");

        // Assert
        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);
        var content = Assert.Single(requestBody.Content!);
        Assert.Equal("multipart/form-data", content.Key);
        Assert.True(requestBody.Required);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

    [Fact]
    public void HandleDefaultIAcceptsMetadataForOptionalFormFileParameter()
    {
        // Arrange
        var operation = GetOpenApiOperation((IFormFile? inferredFormFile) => "");

        // Assert
        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);
        var content = Assert.Single(requestBody.Content!);
        Assert.Equal("multipart/form-data", content.Key);
        Assert.False(requestBody.Required);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

    [Fact]
    public void AddsMultipartFormDataRequestFormatWhenFormFileSpecified()
    {
        // Arrange
        var operation = GetOpenApiOperation((IFormFile file) => Results.NoContent());

        // Assert
        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);
        var content = Assert.Single(requestBody.Content!);
        Assert.Equal("multipart/form-data", content.Key);
        Assert.True(requestBody.Required);
        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

    [Fact]
    public void HasMultipleRequestFormatsWhenFormFileSpecifiedWithConsumesAttribute()
    {
        var operation = GetOpenApiOperation(
            [Consumes("application/custom0", "application/custom1")] (IFormFile file) => Results.NoContent());

        var requestBody = operation.RequestBody;
        Assert.NotNull(requestBody);
        var content = requestBody.Content!;

        Assert.Equal(2, content.Count);

        var requestFormat0 = content["application/custom0"];
        Assert.NotNull(requestFormat0);

        var requestFormat1 = content["application/custom1"];
        Assert.NotNull(requestFormat1);

        Assert.NotNull(operation.Parameters);
        Assert.Empty(operation.Parameters);
    }

    [Fact]
    public void TestIsRequiredFromFormFile()
    {
        var operation0 = GetOpenApiOperation((IFormFile fromFile) => { });
        var operation1 = GetOpenApiOperation((IFormFile? fromFile) => { });
        Assert.NotNull(operation0.RequestBody);
        Assert.NotNull(operation1.RequestBody);

        var fromFileParam0 = operation0.RequestBody;
        var fromFileParam0ContentType = Assert.Single(fromFileParam0.Content!.Values);
        Assert.Equal("multipart/form-data", fromFileParam0.Content.Keys.SingleOrDefault());
        Assert.True(fromFileParam0.Required);

        var fromFileParam1 = operation1.RequestBody;
        var fromFileParam1ContentType = Assert.Single(fromFileParam1.Content!.Values);
        Assert.Equal("multipart/form-data", fromFileParam1.Content.Keys.SingleOrDefault());
        Assert.False(fromFileParam1.Required);
    }

    [Fact]
    public void AddsFromFormParameterAsFormFile()
    {
        static void AssertFormFileParameter(OpenApiOperation operation, JsonSchemaType expectedType, string expectedName)
        {
            var requestBody = operation.RequestBody;
            Assert.NotNull(requestBody);
            var content = Assert.Single(requestBody.Content!);
            Assert.Equal("multipart/form-data", content.Key);
            Assert.NotNull(operation.Parameters);
            Assert.Empty(operation.Parameters);
        }

        AssertFormFileParameter(GetOpenApiOperation((IFormFile file) => { }), JsonSchemaType.Object, "file");
        AssertFormFileParameter(GetOpenApiOperation(([FromForm(Name = "file_name")] IFormFile file) => { }), JsonSchemaType.Object, "file_name");
    }

    [Fact]
    public void AddsMultipartFormDataResponseFormatWhenFormFileCollectionSpecified()
    {
        AssertFormFileCollection((IFormFileCollection files) => Results.NoContent(), "files");
        AssertFormFileCollection(([FromForm] IFormFileCollection uploads) => Results.NoContent(), "uploads");

        static void AssertFormFileCollection(Delegate handler, string expectedName)
        {
            // Arrange
            var operation = GetOpenApiOperation(handler);

            // Assert
            var requestBody = operation.RequestBody;
            Assert.NotNull(requestBody);
            var content = Assert.Single(requestBody.Content!);
            Assert.Equal("multipart/form-data", content.Key);
            Assert.True(requestBody.Required);
            Assert.NotNull(operation.Parameters);
            Assert.Empty(operation.Parameters);
        }
    }

#nullable restore

    [Fact]
    public void HandlesEndpointWithDescriptionAndSummary_WithAttributes()
    {
        var operation = GetOpenApiOperation(
            [EndpointSummary("A summary")][EndpointDescription("A description")] (int id) => "");

        // Assert
        Assert.Equal("A description", operation.Description);
        Assert.Equal("A summary", operation.Summary);
    }

    // Test case for https://github.com/dotnet/aspnetcore/issues/41622
    [Fact]
    public void HandlesEndpointWithMultipleResponses()
    {
        var operation = GetOpenApiOperation(() => TypedResults.Ok(new InferredJsonClass()),
            additionalMetadata: new[]
            {
                // Metadata added by the `IEndpointMetadataProvider` on `TypedResults.Ok`
                new ProducesResponseTypeMetadata(StatusCodes.Status200OK),
                // Metadata added by the `Produces<Type>` extension method
                new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(InferredJsonClass), new[] { "application/json" }),
            });

        var response = Assert.Single(operation.Responses);
        var content = Assert.Single(response.Value.Content);
        Assert.Equal("200", response.Key);
        Assert.Equal("application/json", content.Key);

    }

    [Fact]
    public void OnlyAddParametersWithCorrectLocations()
    {
        var operation = GetOpenApiOperation(([FromBody] int fromBody, [FromRoute] int fromRoute, [FromServices] int fromServices) => { });

        Assert.Single(operation.Parameters);
    }

    [Theory]
    [InlineData("/todos/{id}", "id")]
    [InlineData("/todos/{Id}", "Id")]
    [InlineData("/todos/{id:minlen(2)}", "id")]
    public void FavorsParameterCasingInRoutePattern(string pattern, string expectedName)
    {
        var operation = GetOpenApiOperation((int Id) => "", pattern);

        var param = Assert.Single(operation.Parameters);
        Assert.Equal(expectedName, param.Name);
    }

    [Fact]
    public void HandlesEndpointWithNoRequestBodyOrParams()
    {
        var operationWithNoParams = GetOpenApiOperation(() => "", "/");

        Assert.Empty(operationWithNoParams.Parameters);
        Assert.Null(operationWithNoParams.RequestBody);
    }

    [Fact]
    public void HandlesEndpointWithNoRequestBody()
    {
        var operationWithNoBodyParams = GetOpenApiOperation((int id) => "", "/", httpMethods: new[] { "PUT" });

        Assert.Single(operationWithNoBodyParams.Parameters);
        Assert.Null(operationWithNoBodyParams.RequestBody);
    }

    [Fact]
    public void HandlesParameterWithNameInAttribute()
    {
        static void ValidateParameter(OpenApiOperation operation, string expectedName)
        {
            var parameter = Assert.Single(operation.Parameters);
            Assert.Equal(expectedName, parameter.Name);
        }

        ValidateParameter(GetOpenApiOperation(([FromRoute(Name = "routeName")] string param) => ""), "routeName");
        ValidateParameter(GetOpenApiOperation(([FromRoute(Name = "routeName")] string param) => "", "/{param}"), "routeName");
        ValidateParameter(GetOpenApiOperation(([FromQuery(Name = "queryName")] string param) => ""), "queryName");
        ValidateParameter(GetOpenApiOperation(([FromHeader(Name = "headerName")] string param) => ""), "headerName");
    }

#nullable enable
    public class AsParametersWithRequiredMembers
    {
        public required string RequiredStringMember { get; set; }
        public required string? RequiredNullableStringMember { get; set; }
        public string NonNullableStringMember { get; set; } = string.Empty;
        public string? NullableStringMember { get; set; }
    }

    [Fact]
    public void SupportsRequiredMembersInAsParametersAttribute()
    {
        var operation = GetOpenApiOperation(([AsParameters] AsParametersWithRequiredMembers foo) => { });
        Assert.Equal(4, operation.Parameters?.Count);

        Assert.Collection(operation.Parameters!,
            param => Assert.True(param.Required),
            param => Assert.False(param.Required),
            param => Assert.True(param.Required),
            param => Assert.False(param.Required));
    }
#nullable disable

    public class AsParametersWithRequiredMembersObliviousContext
    {
        public required string RequiredStringMember { get; set; }
        public string OptionalStringMember { get; set; }
    }

    [Fact]
    public void SupportsRequiredMembersInAsParametersObliviousContextAttribute()
    {
        var operation = GetOpenApiOperation(([AsParameters] AsParametersWithRequiredMembersObliviousContext foo) => { });
        Assert.Equal(2, operation.Parameters.Count);

        Assert.Collection(operation.Parameters,
            param => Assert.True(param.Required),
            param => Assert.False(param.Required));
    }

    [Fact]
    public void DoesNotGenerateRequestBodyWhenInferredBodyDisabled()
    {
        var operation = GetOpenApiOperation((string[] names) => { }, httpMethods: new[] { "GET" });

        var parameter = Assert.Single(operation.Parameters);

        Assert.Equal("names", parameter.Name);
        Assert.Equal(ParameterLocation.Query, parameter.In);
        Assert.Null(operation.RequestBody);
    }

    private static OpenApiOperation GetOpenApiOperation(
        Delegate action,
        string pattern = null,
        IEnumerable<string> httpMethods = null,
        string displayName = null,
        object[] additionalMetadata = null)
    {
        var methodInfo = action.Method;
        var attributes = methodInfo.GetCustomAttributes();

        var httpMethodMetadata = new HttpMethodMetadata(httpMethods ?? new[] { "GET" });
        var hostEnvironment = new HostEnvironment() { ApplicationName = nameof(OpenApiOperationGeneratorTests) };
        var metadataItems = new List<object>(attributes) { methodInfo, httpMethodMetadata };
        metadataItems.AddRange(additionalMetadata ?? Array.Empty<object>());
        var endpointMetadata = new EndpointMetadataCollection(metadataItems.ToArray());
        var routePattern = RoutePatternFactory.Parse(pattern ?? "/");

        var generator = new OpenApiGenerator(
            hostEnvironment,
            new ServiceProviderIsService());

        return generator.GetOpenApiOperation(methodInfo, endpointMetadata, routePattern);
    }

    private static void TestAction()
    {
    }

    // Shared with OpenApiRouteHandlerExtensionsTests
    internal class ServiceProviderIsService : IServiceProviderIsService
    {
        public bool IsService(Type serviceType) => serviceType == typeof(IInferredServiceInterface);
    }

    internal class HostEnvironment : IHostEnvironment
    {
        public string EnvironmentName { get; set; }
        public string ApplicationName { get; set; }
        public string ContentRootPath { get; set; }
        public IFileProvider ContentRootFileProvider { get; set; }
    }

    private class InferredJsonClass
    {
    }

    private interface IInferredJsonInterface
    {
    }

    private record TryParseStringRecord(int Value)
    {
        public static bool TryParse(string value, out TryParseStringRecord result) =>
            throw new NotImplementedException();
    }

    private record struct TryParseStringRecordStruct(int Value)
    {
        public static bool TryParse(string value, out TryParseStringRecordStruct result) =>
            throw new NotImplementedException();
    }

    private interface IInferredServiceInterface
    {
    }

    private record BindAsyncRecord(int Value)
    {
        public static ValueTask<BindAsyncRecord> BindAsync(HttpContext context, ParameterInfo parameter) =>
            throw new NotImplementedException();
        public static bool TryParse(string value, out BindAsyncRecord result) =>
            throw new NotImplementedException();
    }

    private record ArgumentListRecord([FromRoute] int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);

    private record struct ArgumentListRecordStruct([FromRoute] int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);

    private record ArgumentListRecordWithoutAttributes(int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);

    private record ArgumentListRecordWithoutPositionalParameters
    {
        [FromRoute]
        public int Foo { get; set; }
        public int Bar { get; set; }
        public InferredJsonClass FromBody { get; set; }
        public HttpContext Context { get; set; }
    }

    private class ArgumentListClass
    {
        [FromRoute]
        public int Foo { get; set; }
        public int Bar { get; set; }
        public InferredJsonClass FromBody { get; set; }
        public HttpContext Context { get; set; }
    }

    private class ArgumentListClassWithReadOnlyProperties : ArgumentListClass
    {
        public int ReadOnly { get; }
    }

    private struct ArgumentListStruct
    {
        [FromRoute]
        public int Foo { get; set; }
        public int Bar { get; set; }
        public InferredJsonClass FromBody { get; set; }
        public HttpContext Context { get; set; }
    }

    private class TestEndpointConventionBuilder : EndpointBuilder, IEndpointConventionBuilder
    {
        public void Add(Action<EndpointBuilder> convention)
        {
            convention(this);
        }

        public override Endpoint Build()
        {
            throw new NotImplementedException();
        }
    }

    private class CustomFromKeyedServicesAttribute(object key) : FromKeyedServicesAttribute(key);

    private class CustomFromServicesAttribute : FromServicesAttribute;
}
