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

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Moq;

namespace Microsoft.AspNetCore.Mvc.Formatters;

public class FormatFilterTests
{
    public enum FormatSource
    {
        RouteData,
        QueryData,
        RouteAndQueryData
    }

    [Theory]
    [InlineData("json", FormatSource.RouteData)]
    [InlineData("json", FormatSource.QueryData)]
    [InlineData("json", FormatSource.RouteAndQueryData)]
    public void FormatFilter_ContextContainsFormat_DefaultFormat(string format, FormatSource place)
    {
        // Arrange
        var mediaType = new StringSegment("application/json");
        var mockObjects = new MockObjects(format, place);

        var resultExecutingContext = mockObjects.CreateResultExecutingContext();
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { });

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);

        // Assert
        Assert.Null(resourceExecutingContext.Result);

        // Act
        filter.OnResultExecuting(resultExecutingContext);

        // Assert
        var objectResult = Assert.IsType<ObjectResult>(resultExecutingContext.Result);
        Assert.Single(objectResult.ContentTypes);
        MediaTypeAssert.Equal(mediaType, objectResult.ContentTypes[0]);
    }

    [Fact]
    public void FormatFilter_ContextContainsFormat_InRouteAndQueryData()
    {
        // If the format is present in both route and query data, the one in route data wins

        // Arrange
        var mediaType = new StringSegment("application/json");
        var mockObjects = new MockObjects("json", FormatSource.RouteData);
        var httpContext = new Mock<HttpContext>();
        httpContext.Setup(c => c.Response).Returns(new Mock<HttpResponse>().Object);

        // Query contains xml
        httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(true);
        httpContext.Setup(c => c.Request.Query["format"]).Returns("xml");

        // RouteData contains json
        var data = new RouteData();
        data.Values.Add("format", "json");

        var ac = new ActionContext(httpContext.Object, data, new ActionDescriptor());

        var resultExecutingContext = new ResultExecutingContext(
            ac,
            new IFilterMetadata[] { },
            new ObjectResult("Hello!"),
            controller: new object());

        var resourceExecutingContext = new ResourceExecutingContext(
            ac,
            new IFilterMetadata[] { },
            new List<IValueProviderFactory>());

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);
        filter.OnResultExecuting(resultExecutingContext);

        // Assert
        var objectResult = Assert.IsType<ObjectResult>(resultExecutingContext.Result);
        Assert.Single(objectResult.ContentTypes);
        MediaTypeAssert.Equal(mediaType, objectResult.ContentTypes[0]);
    }

    [Theory]
    [InlineData("foo", FormatSource.RouteData, "application/foo")]
    [InlineData("foo", FormatSource.QueryData, "application/foo")]
    [InlineData("foo", FormatSource.RouteAndQueryData, "application/foo")]
    public void FormatFilter_ContextContainsFormat_Custom(
        string format,
        FormatSource place,
        string contentType)
    {
        // Arrange
        var mediaType = new StringSegment(contentType);

        var mockObjects = new MockObjects(format, place);
        var resultExecutingContext = mockObjects.CreateResultExecutingContext();
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { });

        mockObjects.MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat(
            format,
            MediaTypeHeaderValue.Parse(contentType));

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);
        filter.OnResultExecuting(resultExecutingContext);

        // Assert
        var objectResult = Assert.IsType<ObjectResult>(resultExecutingContext.Result);
        Assert.Single(objectResult.ContentTypes);
        MediaTypeAssert.Equal(mediaType, objectResult.ContentTypes[0]);
    }

    [Theory]
    [InlineData("foo", FormatSource.RouteData)]
    [InlineData("foo", FormatSource.QueryData)]
    public void FormatFilter_ContextContainsNonExistingFormat(
        string format,
        FormatSource place)
    {
        // Arrange
        var mockObjects = new MockObjects(format, place);
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { });

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);

        // Assert
        var actionResult = resourceExecutingContext.Result;
        Assert.IsType<NotFoundResult>(actionResult);
    }

    [Fact]
    public void FormatFilter_ContextDoesntContainFormat()
    {
        // Arrange
        var mockObjects = new MockObjects();
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { });

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);

        // Assert
        Assert.Null(resourceExecutingContext.Result);
    }

    [Theory]
    [InlineData("json", FormatSource.RouteData, "application/json")]
    [InlineData("json", FormatSource.QueryData, "application/json")]
    public void FormatFilter_ContextContainsFormat_ContainsProducesFilter_Matching(
        string format,
        FormatSource place,
        string contentType)
    {
        // Arrange
        var produces = new ProducesAttribute(contentType, new string[] { "application/foo", "text/bar" });
        var mockObjects = new MockObjects(format, place);
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { produces });

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);

        // Assert
        Assert.Null(resourceExecutingContext.Result);
    }

    [Fact]
    public void FormatFilter_LessSpecificThan_Produces()
    {
        // Arrange
        var produces = new ProducesAttribute("application/xml;version=1", new string[] { });
        var mockObjects = new MockObjects("xml", FormatSource.RouteData);
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { produces });

        mockObjects.MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat(
            "xml",
            MediaTypeHeaderValue.Parse("application/xml"));

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);

        // Assert
        Assert.Null(resourceExecutingContext.Result);
    }

    [Fact]
    public void FormatFilter_MoreSpecificThan_Produces()
    {
        // Arrange
        var produces = new ProducesAttribute("application/xml", new string[] { });
        var mockObjects = new MockObjects("xml", FormatSource.RouteData);
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { produces });

        mockObjects.MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat(
            "xml",
            MediaTypeHeaderValue.Parse("application/xml;version=1"));

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);

        // Assert
        var actionResult = resourceExecutingContext.Result;
        Assert.IsType<NotFoundResult>(actionResult);
    }

    [Theory]
    [InlineData("json", FormatSource.RouteData)]
    [InlineData("json", FormatSource.QueryData)]
    public void FormatFilter_ContextContainsFormat_ContainsProducesFilter_Conflicting(
        string format,
        FormatSource place)
    {
        // Arrange
        var produces = new ProducesAttribute("application/xml", new string[] { "application/foo", "text/bar" });
        var mockObjects = new MockObjects(format, place);
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { produces });

        mockObjects.MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat(
            "xml",
            MediaTypeHeaderValue.Parse("application/xml"));

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);

        // Assert
        var result = Assert.IsType<NotFoundResult>(resourceExecutingContext.Result);
    }

    [Theory]
    [InlineData("", FormatSource.RouteData)]
    [InlineData(null, FormatSource.QueryData)]
    public void FormatFilter_ContextContainsFormat_Invalid(
        string format,
        FormatSource place)
    {
        // Arrange
        var mockObjects = new MockObjects(format, place);
        var resourceExecutingContext = mockObjects.CreateResourceExecutingContext(new IFilterMetadata[] { });
        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);

        // Assert
        Assert.Null(resourceExecutingContext.Result);
    }

    [Theory]
    [InlineData("json", FormatSource.RouteData, "json")]
    [InlineData("json", FormatSource.QueryData, "json")]
    [InlineData("", FormatSource.RouteAndQueryData, null)]
    [InlineData(null, FormatSource.RouteAndQueryData, null)]
    public void FormatFilter_GetFormat(
        string input,
        FormatSource place,
        string expected)
    {
        // Arrange
        var mockObjects = new MockObjects(input, place);
        var context = mockObjects.CreateResultExecutingContext();
        var filterAttribute = new FormatFilterAttribute();
        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        var format = filter.GetFormat(context);

        // Assert
        Assert.Equal(expected, filter.GetFormat(context));
    }

    [Fact]
    [ReplaceCulture("de-CH", "de-CH")]
    public void FormatFilter_GetFormat_UsesInvariantCulture()
    {
        // Arrange
        var mockObjects = new MockObjects();
        var context = mockObjects.CreateResultExecutingContext();
        context.RouteData.Values["format"] = new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7));
        var expected = "10/31/2018 07:37:38 -07:00";
        var filterAttribute = new FormatFilterAttribute();
        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        var format = filter.GetFormat(context);

        // Assert
        Assert.Equal(expected, filter.GetFormat(context));
    }

    [Fact]
    public void FormatFilter_ExplicitContentType_SetOnObjectResult_TakesPrecedence()
    {
        // Arrange
        var mediaType = new StringSegment("application/foo");
        var mockObjects = new MockObjects("json", FormatSource.QueryData);
        var httpContext = new Mock<HttpContext>();
        httpContext.Setup(c => c.Response).Returns(new Mock<HttpResponse>().Object);
        httpContext.Setup(c => c.Request.Query["format"]).Returns("json");
        var actionContext = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor());
        var objectResult = new ObjectResult("Hello!");
        objectResult.ContentTypes.Add(new MediaTypeHeaderValue("application/foo"));
        var resultExecutingContext = new ResultExecutingContext(
            actionContext,
            new IFilterMetadata[] { },
            objectResult,
            controller: new object());

        var resourceExecutingContext = new ResourceExecutingContext(
            actionContext,
            new IFilterMetadata[] { },
            new List<IValueProviderFactory>());

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);
        filter.OnResultExecuting(resultExecutingContext);

        // Assert
        var result = Assert.IsType<ObjectResult>(resultExecutingContext.Result);
        Assert.Single(result.ContentTypes);
        MediaTypeAssert.Equal(mediaType, result.ContentTypes[0]);
    }

    [Fact]
    public void FormatFilter_ExplicitContentType_SetOnResponse_TakesPrecedence()
    {
        // Arrange
        var mediaType = MediaTypeHeaderValue.Parse("application/foo");
        var mockObjects = new MockObjects("json", FormatSource.QueryData);
        var response = new Mock<HttpResponse>();
        response.Setup(r => r.ContentType).Returns("application/foo");
        var httpContext = new Mock<HttpContext>();
        httpContext.Setup(c => c.Response).Returns(response.Object);
        httpContext.Setup(c => c.Request.Query["format"]).Returns("json");
        var actionContext = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor());
        var resultExecutingContext = new ResultExecutingContext(
            actionContext,
            new IFilterMetadata[] { },
            new ObjectResult("Hello!"),
            controller: new object());

        var resourceExecutingContext = new ResourceExecutingContext(
            actionContext,
            new IFilterMetadata[] { },
            new List<IValueProviderFactory>());

        var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance);

        // Act
        filter.OnResourceExecuting(resourceExecutingContext);
        filter.OnResultExecuting(resultExecutingContext);

        // Assert
        var result = Assert.IsType<ObjectResult>(resultExecutingContext.Result);
        Assert.Empty(result.ContentTypes);
    }

    private class MockObjects
    {
        public MvcOptions MvcOptions { get; private set; }
        public HttpContext MockHttpContext { get; private set; }
        public ActionContext MockActionContext { get; private set; }

        public IOptions<MvcOptions> OptionsManager { get; private set; }

        public MockObjects(string format = null, FormatSource? place = null)
        {
            var httpContext = new Mock<HttpContext>();
            httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false);
            httpContext.Setup(c => c.Response).Returns(new Mock<HttpResponse>().Object);
            MockHttpContext = httpContext.Object;

            Initialize(httpContext, format, place);
        }

        public ResourceExecutingContext CreateResourceExecutingContext(IFilterMetadata[] filters)
        {
            var context = new ResourceExecutingContext(
                MockActionContext,
                filters,
                new List<IValueProviderFactory>());
            return context;
        }

        public ResultExecutingContext CreateResultExecutingContext()
        {
            return new ResultExecutingContext(
                MockActionContext,
                new IFilterMetadata[] { },
                new ObjectResult("Some Value"),
                controller: new object());
        }

        private ActionContext CreateMockActionContext(
            Mock<HttpContext> httpContext,
            string format,
            FormatSource? place)
        {
            var data = new RouteData();

            if (place == FormatSource.RouteData || place == FormatSource.RouteAndQueryData)
            {
                data.Values.Add("format", format);
                httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false);
            }

            if (place == FormatSource.QueryData || place == FormatSource.RouteAndQueryData)
            {
                httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(true);
                httpContext.Setup(c => c.Request.Query["format"]).Returns(format);
            }
            else if (place == null && format == null)
            {
                httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false);
            }

            return new ActionContext(httpContext.Object, data, new ActionDescriptor());
        }

        private void Initialize(
            Mock<HttpContext> httpContext,
            string format = null,
            FormatSource? place = null)
        {
            OptionsManager = Options.Create(new MvcOptions());

            // Setup options on mock service provider
            MvcOptions = OptionsManager.Value;

            // Set up default output formatters.
            MvcOptions.OutputFormatters.Add(new HttpNoContentOutputFormatter());
            MvcOptions.OutputFormatters.Add(new StringOutputFormatter());
            MvcOptions.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions()));

            // Set up default mapping for json extensions to content type
            MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat(
                "json",
                MediaTypeHeaderValue.Parse("application/json"));

            // Setup MVC services on mock service provider
            MockActionContext = CreateMockActionContext(httpContext, format, place);
        }
    }
}
