// 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.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Microsoft.AspNetCore.Mvc.ApiExplorer;

public class ApiResponseTypeProviderTest
{
    [Fact]
    public void GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresent()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(
            typeof(GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController),
            nameof(GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController.Get));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new[]
        {
                new ProducesResponseTypeAttribute(201),
                new ProducesResponseTypeAttribute(404),
            });

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format =>
                    {
                        Assert.Equal("application/json", format.MediaType);
                        Assert.IsType<TestOutputFormatter>(format.Formatter);
                    });
            },
            responseType =>
            {
                Assert.Equal(301, responseType.StatusCode);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Empty(responseType.ApiResponseFormats);
            },
            responseType =>
            {
                Assert.Equal(404, responseType.StatusCode);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Empty(responseType.ApiResponseFormats);
            });
    }

    [ApiConventionType(typeof(DefaultApiConventions))]
    public class GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController : ControllerBase
    {
        [Produces(typeof(BaseModel))]
        [ProducesResponseType(301)]
        [ProducesResponseType(404)]
        public Task<ActionResult<BaseModel>> Get(int id) => null;
    }

    [Fact]
    public void GetApiResponseTypes_CombinesFilters()
    {
        // Arrange
        var filterDescriptors = new[]
        {
                new FilterDescriptor(new ProducesResponseTypeAttribute(400), FilterScope.Global),
                new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(object), 201), FilterScope.Controller),
                new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(ProblemDetails), 400), FilterScope.Controller),
                new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(BaseModel), 201), FilterScope.Action),
                new FilterDescriptor(new ProducesResponseTypeAttribute(404), FilterScope.Action),
            };

        var actionDescriptor = new ControllerActionDescriptor
        {
            FilterDescriptors = filterDescriptors,
            MethodInfo = typeof(GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController).GetMethod(nameof(GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController.Get)),
        };

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(201, responseType.StatusCode);
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format =>
                    {
                        Assert.Equal("application/json", format.MediaType);
                        Assert.IsType<TestOutputFormatter>(format.Formatter);
                    });
            },
            responseType =>
            {
                Assert.Equal(400, responseType.StatusCode);
                Assert.Equal(typeof(ProblemDetails), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format =>
                    {
                        Assert.Equal("application/json", format.MediaType);
                        Assert.IsType<TestOutputFormatter>(format.Formatter);
                    });
            },
            responseType =>
            {
                Assert.Equal(404, responseType.StatusCode);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Empty(responseType.ApiResponseFormats);
            });
    }

    [Fact]
    public void GetApiResponseTypes_ReturnsResponseTypesFromApiConventionItem()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(
            typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
            nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));

        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new[]
        {
                new ProducesResponseTypeAttribute(200),
                new ProducesResponseTypeAttribute(400),
                new ProducesResponseTypeAttribute(404),
            });

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
           result.OrderBy(r => r.StatusCode),
           responseType =>
           {
               Assert.Equal(200, responseType.StatusCode);
               Assert.Equal(typeof(BaseModel), responseType.Type);
               Assert.False(responseType.IsDefaultResponse);
               Assert.Collection(
                    responseType.ApiResponseFormats,
                    format =>
                    {
                        Assert.Equal("application/json", format.MediaType);
                        Assert.IsType<TestOutputFormatter>(format.Formatter);
                    });
           },
           responseType =>
           {
               Assert.Equal(400, responseType.StatusCode);
               Assert.Equal(typeof(void), responseType.Type);
               Assert.False(responseType.IsDefaultResponse);
               Assert.Empty(responseType.ApiResponseFormats);
           },
           responseType =>
           {
               Assert.Equal(404, responseType.StatusCode);
               Assert.Equal(typeof(void), responseType.Type);
               Assert.False(responseType.IsDefaultResponse);
               Assert.Empty(responseType.ApiResponseFormats);
           });
    }

    [Fact]
    public void GetApiResponseTypes_ReturnsDescriptionFromProducesResponseType()
    {
        // Arrange

        const string expectedOkDescription = "All is well";
        const string expectedBadRequestDescription = "Invalid request";
        const string expectedNotFoundDescription = "Something was not found";

        var actionDescriptor = GetControllerActionDescriptor(
            typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
            nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));

        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new[]
        {
                new ProducesResponseTypeAttribute(200) { Description = expectedOkDescription},
                new ProducesResponseTypeAttribute(400) { Description = expectedBadRequestDescription },
                new ProducesResponseTypeAttribute(404) { Description = expectedNotFoundDescription },
            });

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
           result.OrderBy(r => r.StatusCode),
           responseType =>
           {
               Assert.Equal(200, responseType.StatusCode);
               Assert.Equal(typeof(BaseModel), responseType.Type);
               Assert.False(responseType.IsDefaultResponse);
               Assert.Equal(expectedOkDescription, responseType.Description);
               Assert.Collection(
                    responseType.ApiResponseFormats,
                    format =>
                    {
                        Assert.Equal("application/json", format.MediaType);
                        Assert.IsType<TestOutputFormatter>(format.Formatter);
                    });
           },
           responseType =>
           {
               Assert.Equal(400, responseType.StatusCode);
               Assert.Equal(typeof(void), responseType.Type);
               Assert.False(responseType.IsDefaultResponse);
               Assert.Empty(responseType.ApiResponseFormats);
               Assert.Equal(expectedBadRequestDescription, responseType.Description);
           },
           responseType =>
           {
               Assert.Equal(404, responseType.StatusCode);
               Assert.Equal(typeof(void), responseType.Type);
               Assert.False(responseType.IsDefaultResponse);
               Assert.Empty(responseType.ApiResponseFormats);
               Assert.Equal(expectedNotFoundDescription, responseType.Description);
           });
    }

    [ApiConventionType(typeof(DefaultApiConventions))]
    public class GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController : ControllerBase
    {
        public Task<ActionResult<BaseModel>> DeleteBase(int id) => null;
    }

    [Fact]
    public void GetApiResponseTypes_ReturnsDefaultResultsIfNoConventionsMatch()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(
            typeof(GetApiResponseTypes_ReturnsDefaultResultsIfNoConventionsMatchController),
            nameof(GetApiResponseTypes_ReturnsDefaultResultsIfNoConventionsMatchController.PostModel));

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
           result.OrderBy(r => r.StatusCode),
           responseType =>
           {
               Assert.Equal(200, responseType.StatusCode);
               Assert.Equal(typeof(BaseModel), responseType.Type);
               Assert.False(responseType.IsDefaultResponse);
               Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
           });
    }

    [ApiConventionType(typeof(DefaultApiConventions))]
    public class GetApiResponseTypes_ReturnsDefaultResultsIfNoConventionsMatchController : ControllerBase
    {
        public Task<ActionResult<BaseModel>> PostModel(int id, BaseModel model) => null;
    }

    [Fact]
    public void GetApiResponseTypes_ReturnsDefaultProblemResponse()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(
            typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
            nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(201),
                new ProducesResponseTypeAttribute(404),
                new ProducesDefaultResponseTypeAttribute(typeof(SerializableError)),
        });

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.True(responseType.IsDefaultResponse);
                Assert.Equal(typeof(SerializableError), responseType.Type);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(201, responseType.StatusCode);
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(404, responseType.StatusCode);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Empty(responseType.ApiResponseFormats);
            });
    }

    public class GetApiResponseTypes_WithApiConventionMethodAndProducesResponseType : ControllerBase
    {
        [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
        [ProducesResponseType(201)]
        [ProducesResponseType(404)]
        public Task<ActionResult<BaseModel>> Put(int id, BaseModel model) => null;
    }

    [Fact]
    public void GetApiResponseTypes_ReturnsValuesFromProducesResponseType_IfApiConventionMethodAndAttributesAreSpecified()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(
            typeof(GetApiResponseTypes_WithApiConventionMethodAndProducesResponseType),
            nameof(GetApiResponseTypes_WithApiConventionMethodAndProducesResponseType.Put));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(200),
                new ProducesResponseTypeAttribute(404),
                new ProducesDefaultResponseTypeAttribute(),
        });

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(201, responseType.StatusCode);
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(404, responseType.StatusCode);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Empty(responseType.ApiResponseFormats);
            });
    }

    [Fact]
    public void GetApiResponseTypes_UsesErrorType_ForClientErrors()
    {
        // Arrange
        var errorType = typeof(InvalidTimeZoneException);
        var actionDescriptor = GetControllerActionDescriptor(
             typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
             nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(200),
                new ProducesResponseTypeAttribute(404),
                new ProducesResponseTypeAttribute(415),
        });

        actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType);

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(404, responseType.StatusCode);
                Assert.Equal(errorType, responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(415, responseType.StatusCode);
                Assert.Equal(errorType, responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            });
    }

    [Fact]
    public void GetApiResponseTypes_UsesErrorType_ForDefaultResponse()
    {
        // Arrange
        var errorType = typeof(ProblemDetails);
        var actionDescriptor = GetControllerActionDescriptor(
             typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
             nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(200),
                new ProducesDefaultResponseTypeAttribute(),
        });

        actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType);

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(errorType, responseType.Type);
                Assert.True(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            });
    }

    [Fact]
    public void GetApiResponseTypes_DoesNotUseErrorType_IfSpecified()
    {
        // Arrange
        var errorType = typeof(InvalidTimeZoneException);
        var actionDescriptor = GetControllerActionDescriptor(
             typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
             nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(200),
                new ProducesResponseTypeAttribute(typeof(DivideByZeroException), 415),
                new ProducesDefaultResponseTypeAttribute(typeof(DivideByZeroException)),
        });

        actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType);

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(typeof(DivideByZeroException), responseType.Type);
                Assert.True(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(415, responseType.StatusCode);
                Assert.Equal(typeof(DivideByZeroException), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            });
    }

    [Fact]
    public void GetApiResponseTypes_DoesNotUseErrorType_ForNonClientErrors()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(
             typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
             nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(201),
                new ProducesResponseTypeAttribute(300),
                new ProducesResponseTypeAttribute(500),
        });

        actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(InvalidTimeZoneException));

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(201, responseType.StatusCode);
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(300, responseType.StatusCode);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.Empty(responseType.ApiResponseFormats);
            },
            responseType =>
            {
                Assert.Equal(500, responseType.StatusCode);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.Empty(responseType.ApiResponseFormats);
            });
    }

    [Fact]
    public void GetApiResponseTypes_AllowsUsingVoid()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(
             typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
             nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(typeof(InvalidCastException), 400),
                new ProducesResponseTypeAttribute(415),
                new ProducesDefaultResponseTypeAttribute(),
        });

        actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(void));

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.True(responseType.IsDefaultResponse);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.Empty(responseType.ApiResponseFormats);
            },
            responseType =>
            {
                Assert.Equal(400, responseType.StatusCode);
                Assert.Equal(typeof(InvalidCastException), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(415, responseType.StatusCode);
                Assert.Equal(typeof(void), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Empty(responseType.ApiResponseFormats);
            });
    }

    [Fact]
    public void GetApiResponseTypes_CombinesProducesAttributeAndConventions()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel));
        actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/json"), FilterScope.Controller));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(200),
                new ProducesResponseTypeAttribute(400),
                new ProducesDefaultResponseTypeAttribute(),
        });
        actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.True(responseType.IsDefaultResponse);
                Assert.Equal(typeof(ProblemDetails), responseType.Type);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(typeof(DerivedModel), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            },
            responseType =>
            {
                Assert.Equal(400, responseType.StatusCode);
                Assert.Equal(typeof(ProblemDetails), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            });
    }

    [Fact]
    public void GetApiResponseTypes_DoesNotCombineProducesAttributeThatSpecifiesType()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel));
        actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/json") { Type = typeof(string) }, FilterScope.Controller));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(200),
                new ProducesResponseTypeAttribute(400),
                new ProducesDefaultResponseTypeAttribute(),
        });
        actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(typeof(string), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            });
    }

    [Fact]
    public void GetApiResponseTypes_DoesNotCombineProducesResponseTypeAttributeThatSpecifiesStatusCode()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel));
        actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
        {
                new ProducesResponseTypeAttribute(200),
        });
        actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(typeof(DerivedModel), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format => Assert.Equal("application/json", format.MediaType));
            });
    }

    [Fact]
    public void GetApiResponseTypes_UsesContentTypeWithoutWildCard_WhenNoFormatterSupportsIt()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetUser));
        actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/pdf"), FilterScope.Action));

        var provider = GetProvider();

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(typeof(DerivedModel), responseType.Type);
                Assert.False(responseType.IsDefaultResponse);
                Assert.Collection(
                    responseType.ApiResponseFormats,
                    format =>
                    {
                        Assert.Equal("application/pdf", format.MediaType);
                        Assert.Null(format.Formatter);
                    });
            });
    }

    [Fact]
    public void GetApiResponseTypes_HandlesActionWithMultipleContentTypesAndProduces()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetUser));
        actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("text/xml") { Type = typeof(BaseModel) }, FilterScope.Action));
        actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(ValidationProblemDetails), 400, "application/problem+json"), FilterScope.Action));
        actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(ProblemDetails), 404, "application/problem+json"), FilterScope.Action));
        actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesResponseTypeAttribute(409), FilterScope.Action));

        var provider = new ApiResponseTypeProvider(new EmptyModelMetadataProvider(), new ActionResultTypeMapper(), new MvcOptions());

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.Collection(
            result.OrderBy(r => r.StatusCode),
            responseType =>
            {
                Assert.Equal(typeof(BaseModel), responseType.Type);
                Assert.Equal(200, responseType.StatusCode);
                Assert.Equal(new[] { "text/xml" }, GetSortedMediaTypes(responseType));

            },
            responseType =>
            {
                Assert.Equal(typeof(ValidationProblemDetails), responseType.Type);
                Assert.Equal(400, responseType.StatusCode);
                Assert.Equal(new[] { "application/problem+json" }, GetSortedMediaTypes(responseType));
            },
            responseType =>
            {
                Assert.Equal(typeof(ProblemDetails), responseType.Type);
                Assert.Equal(404, responseType.StatusCode);
                Assert.Equal(new[] { "application/problem+json" }, GetSortedMediaTypes(responseType));
            },
            responseType =>
            {
                Assert.Equal(typeof(void), responseType.Type);
                Assert.Equal(409, responseType.StatusCode);
                Assert.Empty(GetSortedMediaTypes(responseType));
            });
    }

    [Fact]
    public void GetApiResponseTypes_ReturnNoResponseTypes_IfActionWithBuiltIResultReturnType()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetIResult));
        var provider = new ApiResponseTypeProvider(new EmptyModelMetadataProvider(), new ActionResultTypeMapper(), new MvcOptions());

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        Assert.False(result.Any());
    }

    [Fact]
    public void GetApiResponseTypes_ReturnResponseType_IfActionHasCustomIResultReturnTypeInMetadata()
    {
        // Arrange
        var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetCustomIResult));
        actionDescriptor.EndpointMetadata = [new ProducesResponseTypeMetadata(200, typeof(MyResponse))];
        var provider = new ApiResponseTypeProvider(new EmptyModelMetadataProvider(), new ActionResultTypeMapper(), new MvcOptions());

        // Act
        var result = provider.GetApiResponseTypes(actionDescriptor);

        // Assert
        var response = Assert.Single(result);
        Assert.Equal(typeof(MyResponse), response.Type);
        Assert.Equal(200, response.StatusCode);
    }

    private static ApiResponseTypeProvider GetProvider()
    {
        var mvcOptions = new MvcOptions
        {
            OutputFormatters = { new TestOutputFormatter() },
        };
        var provider = new ApiResponseTypeProvider(new EmptyModelMetadataProvider(), new ActionResultTypeMapper(), mvcOptions);
        return provider;
    }

    private static IEnumerable<string> GetSortedMediaTypes(ApiResponseType apiResponseType)
    {
        return apiResponseType.ApiResponseFormats
            .OrderBy(format => format.MediaType)
            .Select(format => format.MediaType);
    }

    private static ControllerActionDescriptor GetControllerActionDescriptor(Type type, string name)
    {
        var method = type.GetMethod(name);
        var actionDescriptor = new ControllerActionDescriptor
        {
            MethodInfo = method,
            FilterDescriptors = new List<FilterDescriptor>(),
        };

        foreach (var filterAttribute in method.GetCustomAttributes().OfType<IFilterMetadata>())
        {
            actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(filterAttribute, FilterScope.Action));
        }

        return actionDescriptor;
    }

    public class BaseModel { }

    public class DerivedModel : BaseModel { }

    public class TestController
    {
        public ActionResult<DerivedModel> GetUser(int id) => null;

        public ActionResult<DerivedModel> GetUserLocation(int a, int b) => null;

        public ActionResult<DerivedModel> PutModel(string userId, DerivedModel model) => null;

        public IResult GetIResult(int id) => null;

        public MyResponse GetCustomIResult() => new MyResponse { Content = "Test Content" };
    }

    public class MyResponse : IResult
    {
        public required string Content { get; set; }

        public Task ExecuteAsync(HttpContext httpContext)
        {
            return httpContext.Response.WriteAsJsonAsync(this);
        }
    }

    private class TestOutputFormatter : OutputFormatter
    {
        public TestOutputFormatter()
        {
            SupportedMediaTypes.Add(new Net.Http.Headers.MediaTypeHeaderValue("application/json"));
        }

        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context) => Task.CompletedTask;
    }

    public static class SearchApiConventions
    {
        [ProducesResponseType(206)]
        [ProducesResponseType(406)]
        public static void Search(object searchTerm, int page) { }
    }
}
