// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Formatters
{
    public class OutputFormatterTests
    {
        [Fact]
        public void CanWriteResult_ForNullContentType_UsesFirstEntryInSupportedContentTypes()
        {
            // Arrange
            var context = new OutputFormatterWriteContext(
                new DefaultHttpContext(),
                new TestHttpResponseStreamWriterFactory().CreateWriter,
                objectType: null,
                @object: null);

            var formatter = new TestOutputFormatter();

            // Act
            var result = formatter.CanWriteResult(context);

            // Assert
            Assert.True(result);
            Assert.Equal(formatter.SupportedMediaTypes[0].ToString(), context.ContentType.ToString());
        }

        [Fact]
        public void GetSupportedContentTypes_ReturnsNull_ForUnsupportedType()
        {
            // Arrange
            var formatter = new TypeSpecificFormatter();
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
            formatter.SupportedTypes.Add(typeof(int));

            // Act
            var contentTypes = formatter.GetSupportedContentTypes(
                contentType: null,
                objectType: typeof(string));

            // Assert
            Assert.Null(contentTypes);
        }

        [Fact]
        public void CanWrite_ReturnsFalse_ForUnsupportedType()
        {
            // Arrange
            var formatter = new TypeSpecificFormatter();
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
            formatter.SupportedTypes.Add(typeof(int));

            var context = new OutputFormatterWriteContext(
                new DefaultHttpContext(),
                new TestHttpResponseStreamWriterFactory().CreateWriter,
                typeof(string),
                "Hello, world!")
            {
                ContentType = new StringSegment(formatter.SupportedMediaTypes[0].ToString()),
            };

            // Act
            var result = formatter.CanWriteResult(context);

            // Assert
            Assert.False(result);
        }

        [Theory]
        [InlineData(true, true)]
        [InlineData(false, false)]
        public void CanWriteResult_MatchesWildcardsOnlyWhenContentTypeProvidedByServer(
            bool contentTypeProvidedByServer, bool shouldMatchWildcards)
        {
            // Arrange
            var formatter = new TypeSpecificFormatter();
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+xml"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+json"));
            formatter.SupportedTypes.Add(typeof(string));

            var requestedContentType = "application/vnd.test.entity+json;v=2";
            var context = new OutputFormatterWriteContext(
                new DefaultHttpContext(),
                new TestHttpResponseStreamWriterFactory().CreateWriter,
                typeof(string),
                "Hello, world!")
            {
                ContentType = new StringSegment(requestedContentType),
                ContentTypeIsServerDefined = contentTypeProvidedByServer,
            };

            // Act
            var result = formatter.CanWriteResult(context);

            // Assert
            Assert.Equal(shouldMatchWildcards, result);
            Assert.Equal(requestedContentType, context.ContentType.ToString());
        }

        [Fact]
        public void GetSupportedContentTypes_ReturnsAllNonWildcardContentTypes_WithContentTypeNull()
        {
            // Arrange
            var formatter = new TestOutputFormatter();
            formatter.SupportedMediaTypes.Clear();
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("*/*"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/*"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain;*"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));

            // Act
            var contentTypes = formatter.GetSupportedContentTypes(
                contentType: null,
                objectType: typeof(string));

            // Assert
            Assert.Equal(2, contentTypes.Count);
            Assert.Single(contentTypes, ct => ct.ToString() == "application/json");
            Assert.Single(contentTypes, ct => ct.ToString() == "application/xml");
        }

        [Fact]
        public void GetSupportedContentTypes_ReturnsMoreSpecificMatchingContentTypes_WithContentType()
        {
            // Arrange
            var formatter = new TestOutputFormatter();

            formatter.SupportedMediaTypes.Clear();
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));

            // Act
            var contentTypes = formatter.GetSupportedContentTypes(
                "application/*",
                typeof(int));

            // Assert
            var contentType = Assert.Single(contentTypes);
            Assert.Equal("application/json", contentType.ToString());
        }

        [Fact]
        public void GetSupportedContentTypes_ReturnsMatchingWildcardContentTypes_WithContentType()
        {
            // Arrange
            var formatter = new TestOutputFormatter();

            formatter.SupportedMediaTypes.Clear();
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+json"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));

            // Act
            var contentTypes = formatter.GetSupportedContentTypes(
                "application/vnd.test+json;v=2",
                typeof(int));

            // Assert
            var contentType = Assert.Single(contentTypes);
            Assert.Equal("application/vnd.test+json;v=2", contentType.ToString());
        }

        [Fact]
        public void GetSupportedContentTypes_ReturnsMatchingContentTypes_NoMatches()
        {
            // Arrange
            var formatter = new TestOutputFormatter();

            formatter.SupportedMediaTypes.Clear();
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+xml"));
            formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));

            // Act
            var contentTypes = formatter.GetSupportedContentTypes(
                "application/vnd.test+bson",
                typeof(int));

            // Assert
            Assert.Null(contentTypes);
        }

        private class TypeSpecificFormatter : OutputFormatter
        {
            public List<Type> SupportedTypes { get; } = new List<Type>();

            protected override bool CanWriteType(Type type)
            {
                return SupportedTypes.Contains(type);
            }

            public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
            {
                throw new NotImplementedException();
            }
        }

        [Fact]
        public void CanWrite_ThrowsInvalidOperationException_IfMediaTypesListIsEmpty()
        {
            // Arrange
            var formatter = new TestOutputFormatter();
            formatter.SupportedMediaTypes.Clear();

            var context = new OutputFormatterWriteContext(
                new DefaultHttpContext(),
                (s, e) => new StreamWriter(s, e),
                typeof(object),
                new object());

            // Act & Assert
            Assert.Throws<InvalidOperationException>(() => formatter.CanWriteResult(context));
        }

        [Fact]
        public void GetSupportedContentTypes_ThrowsInvalidOperationException_IfMediaTypesListIsEmpty()
        {
            // Arrange
            var formatter = new TestOutputFormatter();
            formatter.SupportedMediaTypes.Clear();

            // Act & Assert
            Assert.Throws<InvalidOperationException>(
                () => formatter.GetSupportedContentTypes("application/json", typeof(object)));
        }

        private class TestOutputFormatter : OutputFormatter
        {
            public TestOutputFormatter()
            {
                SupportedMediaTypes.Add("application/acceptCharset");
            }

            public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
            {
                return Task.FromResult(true);
            }
        }
    }
}
