// 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.Collections.Generic;
using System.Linq;
using System.Net.Http.Formatting.Mocks;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.AspNetCore.Testing;
using Microsoft.TestCommon;
using Newtonsoft.Json.Linq;
using Xunit;

namespace System.Net.Http.Formatting
{
    public class DefaultContentNegotiatorTests
    {
        private readonly DefaultContentNegotiator _negotiator = new DefaultContentNegotiator();
        private readonly HttpRequestMessage _request = new HttpRequestMessage();

        public static TheoryData<string, string[], string> MatchRequestMediaTypeData
        {
            get
            {
                // string requestMediaType, string[] supportedMediaTypes, string expectedMediaType
                return new TheoryData<string, string[], string>
                {
                    { "text/plain", new string[0], null },
                    { "text/plain", new string[] { "text/xml", "application/xml" }, null },
                    { "application/xml", new string[] { "application/xml", "text/xml" }, "application/xml" },
                    { "APPLICATION/XML", new string[] { "text/xml", "application/xml" }, "application/xml" },
                    { "application/xml; charset=utf-8", new string[] { "text/xml", "application/xml" }, "application/xml" },
                    { "application/xml; charset=utf-8; parameter=value", new string[] { "text/xml", "application/xml" }, "application/xml" },
                };
            }
        }

        public static TheoryData<string[], string[], string, double, int> MatchAcceptHeaderData
        {
            get
            {
                // string[] acceptHeader, string[] supportedMediaTypes, string expectedMediaType, double matchQuality, int range
                return new TheoryData<string[], string[], string, double, int>
                {
                    { new string[] { "text/plain" }, new string[0], null, 0.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },

                    { new string[] { "text/plain" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None },
                    { new string[] { "text/plain; q=0.5" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None },

                    { new string[] { "application/xml" }, new string[] { "application/xml", "text/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },
                    { new string[] { "APPLICATION/XML; q=0.5" }, new string[] { "text/xml", "application/xml" }, "application/xml", 0.5, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },
                    { new string[] { "text/xml; q=0.5", "APPLICATION/XML; q=0.7" }, new string[] { "text/xml", "application/xml" }, "application/xml", 0.7, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },
                    { new string[] { "application/xml; q=0.0" }, new string[] { "application/xml", "text/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None },
                    { new string[] { "APPLICATION/XML; q=0.0" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None },
                    { new string[] { "text/xml; q=0.0", "APPLICATION/XML; q=0.0" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None },

                    { new string[] { "text/*" }, new string[] { "text/xml", "application/xml" }, "text/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange },
                    { new string[] { "text/*", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },
                    { new string[] { "text/*", "application/xml; q=0.5" }, new string[] { "text/xml", "application/xml" }, "text/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange },
                    { new string[] { "text/*; q=0.5" }, new string[] { "text/xml", "application/xml" }, "text/xml", 0.5, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange },
                    { new string[] { "text/*; q=0.5", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },
                    { new string[] { "text/*; q=0.0", "application/xml; q=0.0" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None },
                    { new string[] { "text/*; q=0.0", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },

                    { new string[] { "*/*; q=0.5" }, new string[] { "text/xml", "application/xml" }, "text/xml", 0.5, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange },
                    { new string[] { "*/*; q=0.0" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None },
                    { new string[] { "*/*; q=0.5", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },
                    { new string[] { "*/*; q=1.0", "application/xml; q=0.5" }, new string[] { "text/xml", "application/xml" }, "text/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange },
                    { new string[] { "*/*", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },

                    { new string[] { "text/*; q=0.5", "*/*; q=0.2", "application/xml; q=1.0" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0,  (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },

                    { new string[] { "application/xml; q=0.5" }, new string[] { "text/xml", "application/xml" }, "application/xml", 0.5, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral },
                };
            }
        }

        public static TheoryData<bool, string[], bool> ShouldMatchOnTypeData
        {
            get
            {
                // bool excludeMatchOnType, string[] acceptHeaders, bool expectedResult
                return new TheoryData<bool, string[], bool>
                {
                    { false, new string[0], true },
                    { true, new string[0], true },

                    { false, new string[] { "application/xml" }, true },
                    { true, new string[] { "application/xml" }, false },

                    { false, new string[] { "application/xml; q=1.0" }, true },
                    { true, new string[] { "application/xml; q=1.0" }, false },

                    { false, new string[] { "application/xml; q=0.0" }, true },
                    { true, new string[] { "application/xml; q=0.0" }, false },

                    { false, new string[] { "application/xml; q=0.0", "application/json" }, true },
                    { true, new string[] { "application/xml; q=0.0", "application/json" }, false },

                    { false, new string[] { "text/nomatch" }, true },
                    { true, new string[] { "text/nomatch" }, false },
                };
            }
        }

        public static TheoryData<string[], string> MatchTypeData
        {
            get
            {
                // string[] supportedMediaTypes, string expectedMediaType
                return new TheoryData<string[], string>
                {
                    { new string[0], "application/octet-stream" },

                    { new string[] { "text/xml", "application/xml" }, "text/xml" },
                    { new string[] { "application/xml", "text/xml" }, "application/xml" },
                };
            }
        }

        public static TheoryData<string[], string, string[], string> SelectResponseCharacterEncodingData
        {
            get
            {
                // string[] acceptEncodings, string requestEncoding, string[] supportedEncodings, string expectedEncoding
                return new TheoryData<string[], string, string[], string>
                {
                    { new string[] { "utf-8" }, null, new string[0], null },
                    { new string[0], "utf-8", new string[0], null },

                    { new string[0], null, new string[] { "utf-8", "utf-16"}, "utf-8" },
                    { new string[0], "utf-16", new string[] { "utf-8", "utf-16"}, "utf-16" },

                    { new string[] { "utf-8" }, null, new string[] { "utf-8", "utf-16"}, "utf-8" },
                    { new string[] { "utf-16" }, "utf-8", new string[] { "utf-8", "utf-16"}, "utf-16" },
                    { new string[] { "utf-16; q=0.5" }, "utf-8", new string[] { "utf-8", "utf-16"}, "utf-16" },

                    { new string[] { "utf-8; q=0.0" }, null, new string[] { "utf-8", "utf-16"}, "utf-8" },
                    { new string[] { "utf-8; q=0.0" }, "utf-16", new string[] { "utf-8", "utf-16"}, "utf-16" },
                    { new string[] { "utf-8; q=0.0", "utf-16; q=0.0" }, "utf-16", new string[] { "utf-8", "utf-16"}, "utf-16" },
                    { new string[] { "utf-8; q=0.0", "utf-16; q=0.0" }, null, new string[] { "utf-8", "utf-16"}, "utf-8" },
                    { new string[] { "*; q=0.0" }, null, new string[] { "utf-8", "utf-16"}, "utf-8" },
                    { new string[] { "*; q=0.0" }, "utf-16", new string[] { "utf-8", "utf-16"}, "utf-16" },
                };
            }
        }

        public static TheoryData<ICollection<MediaTypeFormatterMatch>, MediaTypeFormatterMatch> SelectResponseMediaTypeData
        {
            get
            {
                MediaTypeFormatterMatch matchAccept10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral);
                MediaTypeFormatterMatch matchAccept05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral);

                MediaTypeFormatterMatch matchAcceptSubTypeRange10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange);
                MediaTypeFormatterMatch matchAcceptSubTypeRange05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange);

                MediaTypeFormatterMatch matchAcceptAllRange10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange);
                MediaTypeFormatterMatch matchAcceptAllRange05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange);

                MediaTypeFormatterMatch matchRequest10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestMediaType);
                MediaTypeFormatterMatch matchType10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnCanWriteType);

                // ICollection<MediaTypeFormatterMatch> candidateMatches, MediaTypeFormatterMatch winner
                return new TheoryData<ICollection<MediaTypeFormatterMatch>, MediaTypeFormatterMatch>
                {
                    { new List<MediaTypeFormatterMatch>(), null },
                    { new List<MediaTypeFormatterMatch>() { matchType10 }, matchType10 },
                    { new List<MediaTypeFormatterMatch>() { matchType10, matchRequest10 }, matchRequest10 },
                    { new List<MediaTypeFormatterMatch>() { matchType10, matchRequest10, matchAcceptAllRange10 }, matchAcceptAllRange10 },
                    { new List<MediaTypeFormatterMatch>() { matchType10, matchRequest10, matchAcceptAllRange10, matchAcceptSubTypeRange10 }, matchAcceptSubTypeRange10 },
                    { new List<MediaTypeFormatterMatch>() { matchType10, matchRequest10, matchAcceptAllRange10, matchAcceptSubTypeRange10, matchAccept10 }, matchAccept10 },
                    { new List<MediaTypeFormatterMatch>() { matchAccept05, matchAccept10 }, matchAccept10 },
                    { new List<MediaTypeFormatterMatch>() { matchAccept10, matchAccept05 }, matchAccept10 },

                    { new List<MediaTypeFormatterMatch>() { matchAcceptSubTypeRange05, matchAcceptSubTypeRange10 }, matchAcceptSubTypeRange10 },
                    { new List<MediaTypeFormatterMatch>() { matchAcceptSubTypeRange10, matchAcceptSubTypeRange05 }, matchAcceptSubTypeRange10 },

                    { new List<MediaTypeFormatterMatch>() { matchAcceptAllRange05, matchAcceptAllRange10 }, matchAcceptAllRange10 },
                    { new List<MediaTypeFormatterMatch>() { matchAcceptAllRange10, matchAcceptAllRange05 }, matchAcceptAllRange10 },
                };
            }
        }

        public static TheoryData<MediaTypeFormatterMatch, MediaTypeFormatterMatch, bool> UpdateBestMatchData
        {
            get
            {
                MediaTypeFormatterMatch matchMapping10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.None);
                MediaTypeFormatterMatch matchMapping05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.None);

                // MediaTypeFormatterMatch current, MediaTypeFormatterMatch potentialReplacement, currentWins
                return new TheoryData<MediaTypeFormatterMatch, MediaTypeFormatterMatch, bool>
                {
                    { null, matchMapping10, false },
                    { null, matchMapping05, false },

                    { matchMapping10, matchMapping10, true },
                    { matchMapping10, matchMapping05, true },

                    { matchMapping05, matchMapping10, false },
                    { matchMapping05, matchMapping05, true },
                };
            }
        }

        private static MediaTypeFormatterMatch CreateMatch(double? quality, MediaTypeFormatterMatchRanking ranking)
        {
            MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
            MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/test");
            return new MediaTypeFormatterMatch(formatter, mediaType, quality, ranking);
        }

        [Fact]
        public void TypeIsCorrect()
        {
            new TypeAssert().HasProperties(typeof(DefaultContentNegotiator), TypeAssert.TypeProperties.IsPublicVisibleClass);
        }

        [Fact]
        public void Negotiate_ForEmptyFormatterCollection_ReturnsNull()
        {
            var result = _negotiator.Negotiate(typeof(string), _request, Enumerable.Empty<MediaTypeFormatter>());

            Assert.Null(result);
        }


        [Fact]
        public void Negotiate_ForRequestReturnsFirstMatchingFormatter()
        {
            MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/myMediaType");

            MediaTypeFormatter formatter1 = new MockMediaTypeFormatter()
            {
                CanWriteTypeCallback = (Type t) => false
            };

            MediaTypeFormatter formatter2 = new MockMediaTypeFormatter()
            {
                CanWriteTypeCallback = (Type t) => true
            };

            formatter2.SupportedMediaTypes.Add(mediaType);

            MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(
                new MediaTypeFormatter[]
                {
                    formatter1,
                    formatter2
                });

            _request.Content = new StringContent("test", Encoding.UTF8, mediaType.MediaType);

            var result = _negotiator.Negotiate(typeof(string), _request, collection);
            Assert.Same(formatter2, result.Formatter);
            new MediaTypeAssert().AreEqual(mediaType, result.MediaType, "Expected the formatter's media type to be returned.");
        }

        [Fact]
        public void Negotiate_SelectsJsonAsDefaultFormatter()
        {
            // Arrange
            _request.Content = new StringContent("test");

            // Act
            var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection());

            // Assert
            Assert.IsType<JsonMediaTypeFormatter>(result.Formatter);
            Assert.Equal(MediaTypeConstants.ApplicationJsonMediaType.MediaType, result.MediaType.MediaType);
        }

        [Fact]
        public void Negotiate_SelectsXmlFormatter_ForXhrRequestThatAcceptsXml()
        {
            // Arrange
            _request.Content = new StringContent("test");
            _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
            _request.Headers.Add("x-requested-with", "XMLHttpRequest");

            // Act
            var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection());

            // Assert
            Assert.Equal("application/xml", result.MediaType.MediaType);
            Assert.IsType<XmlMediaTypeFormatter>(result.Formatter);
        }

        [Fact]
        public void Negotiate_SelectsJsonFormatter_ForXhrRequestThatDoesNotSpecifyAcceptHeaders()
        {
            // Arrange
            _request.Content = new StringContent("test");
            _request.Headers.Add("x-requested-with", "XMLHttpRequest");

            // Act
            var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection());

            // Assert
            Assert.Equal("application/json", result.MediaType.MediaType);
            Assert.IsType<JsonMediaTypeFormatter>(result.Formatter);
        }

        [Fact]
        public void Negotiate_SelectsJsonFormatter_ForXHRAndJsonValueResponse()
        {
            // Arrange
            _request.Content = new StringContent("test");
            _request.Headers.Add("x-requested-with", "XMLHttpRequest");

            // Act
            // Mono issue - https://github.com/aspnet/External/issues/27
            var type = TestPlatformHelper.IsMono ? typeof(string) : typeof(JToken);
            var result = _negotiator.Negotiate(type, _request, new MediaTypeFormatterCollection());

            Assert.Equal("application/json", result.MediaType.MediaType);
            Assert.IsType<JsonMediaTypeFormatter>(result.Formatter);
        }

        [Fact]
        public void Negotiate_SelectsJsonFormatter_ForXHRAndMatchAllAcceptHeader()
        {
            // Accept
            _request.Content = new StringContent("test");
            _request.Headers.Add("x-requested-with", "XMLHttpRequest");
            _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));

            // Act
            var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection());

            // Assert
            Assert.Equal("application/json", result.MediaType.MediaType);
            Assert.IsType<JsonMediaTypeFormatter>(result.Formatter);
        }

        [Fact]
        public void Negotiate_UsesRequestedFormatterForXHRAndMatchAllPlusOtherAcceptHeader()
        {
            // Arrange
            _request.Content = new StringContent("test");
            _request.Headers.Add("x-requested-with", "XMLHttpRequest");
            _request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); // XHR header sent by Firefox 3b5

            // Act
            var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection());

            // Assert
            Assert.Equal("application/xml", result.MediaType.MediaType);
            Assert.IsType<XmlMediaTypeFormatter>(result.Formatter);
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void Negotiate_ObservesExcludeMatchOnTypeOnly(bool excludeMatchOnTypeOnly)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator(excludeMatchOnTypeOnly);
            _request.Content = new StringContent("test");
            _request.Headers.Accept.ParseAdd("text/html");

            // Act
            var result = negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection());

            // Assert
            if (excludeMatchOnTypeOnly)
            {
                Assert.Null(result);
            }
            else
            {
                Assert.NotNull(result);
                Assert.Equal("application/json", result.MediaType.MediaType);
            }
        }

        [Theory]
        [MemberData(nameof(MatchAcceptHeaderData))]
        public void MatchAcceptHeader_ReturnsMatch(string[] acceptHeaders, string[] supportedMediaTypes, string expectedMediaType, double expectedQuality, int ranking)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator();

            List<MediaTypeWithQualityHeaderValue> unsortedAcceptHeaders = acceptHeaders.Select(a => MediaTypeWithQualityHeaderValue.Parse(a)).ToList();
            IEnumerable<MediaTypeWithQualityHeaderValue> sortedAcceptHeaders = negotiator.SortMediaTypeWithQualityHeaderValuesByQFactor(unsortedAcceptHeaders);

            MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
            foreach (string supportedMediaType in supportedMediaTypes)
            {
                formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(supportedMediaType));
            }

            // Act
            MediaTypeFormatterMatch match = negotiator.MatchAcceptHeader(sortedAcceptHeaders, formatter);

            // Assert
            if (expectedMediaType == null)
            {
                Assert.Null(match);
            }
            else
            {
                Assert.Same(formatter, match.Formatter);
                Assert.Equal(MediaTypeHeaderValue.Parse(expectedMediaType), match.MediaType);
                Assert.Equal(expectedQuality, match.Quality);
                Assert.Equal(ranking, (int)match.Ranking);
            }
        }

        [Theory]
        [MemberData(nameof(MatchRequestMediaTypeData))]
        public void MatchRequestMediaType_ReturnsMatch(string requestMediaType, string[] supportedMediaTypes, string expectedMediaType)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator();

            HttpRequestMessage request = new HttpRequestMessage();
            request.Content = new StringContent(String.Empty);
            request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(requestMediaType);

            MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
            foreach (string supportedMediaType in supportedMediaTypes)
            {
                formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(supportedMediaType));
            }

            // Act
            MediaTypeFormatterMatch match = negotiator.MatchRequestMediaType(request, formatter);

            // Assert
            if (expectedMediaType == null)
            {
                Assert.Null(match);
            }
            else
            {
                Assert.Same(formatter, match.Formatter);
                Assert.Equal(MediaTypeHeaderValue.Parse(expectedMediaType), match.MediaType);
                Assert.Equal(1.0, match.Quality);
                Assert.Equal(MediaTypeFormatterMatchRanking.MatchOnRequestMediaType, match.Ranking);
            }
        }

        [Theory]
        [MemberData(nameof(ShouldMatchOnTypeData))]
        public void ShouldMatchOnType_ReturnsExpectedResult(bool excludeMatchOnType, string[] acceptHeaders, bool expectedResult)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator(excludeMatchOnType);
            List<MediaTypeWithQualityHeaderValue> unsortedAcceptHeaders = acceptHeaders.Select(a => MediaTypeWithQualityHeaderValue.Parse(a)).ToList();
            IEnumerable<MediaTypeWithQualityHeaderValue> sortedAcceptHeaders = negotiator.SortMediaTypeWithQualityHeaderValuesByQFactor(unsortedAcceptHeaders);

            // Act
            bool result = negotiator.ShouldMatchOnType(sortedAcceptHeaders);

            // Assert
            Assert.Equal(expectedResult, result);
        }

        [Theory]
        [MemberData(nameof(MatchTypeData))]
        public void MatchType_ReturnsMatch(string[] supportedMediaTypes, string expectedMediaType)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator();

            MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
            foreach (string supportedMediaType in supportedMediaTypes)
            {
                formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(supportedMediaType));
            }

            // Act
            MediaTypeFormatterMatch match = negotiator.MatchType(typeof(object), formatter);

            // Assert
            Assert.Same(formatter, match.Formatter);
            Assert.Equal(MediaTypeHeaderValue.Parse(expectedMediaType), match.MediaType);
            Assert.Equal(1.0, match.Quality);
            Assert.Equal(MediaTypeFormatterMatchRanking.MatchOnCanWriteType, match.Ranking);
        }

        [Theory]
        [MemberData(nameof(SelectResponseMediaTypeData))]
        public void SelectResponseMediaTypeFormatter_SelectsMediaType(ICollection<MediaTypeFormatterMatch> matches, MediaTypeFormatterMatch expectedWinner)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator();

            // Act
            MediaTypeFormatterMatch actualWinner = negotiator.SelectResponseMediaTypeFormatter(matches);

            // Assert
            Assert.Same(expectedWinner, actualWinner);
        }

        [Theory]
        [MemberData(nameof(SelectResponseCharacterEncodingData))]
        public void SelectResponseCharacterEncoding_SelectsEncoding(string[] acceptCharsetHeaders, string requestEncoding, string[] supportedEncodings, string expectedEncoding)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator();

            HttpRequestMessage request = new HttpRequestMessage();
            foreach (string acceptCharsetHeader in acceptCharsetHeaders)
            {
                request.Headers.AcceptCharset.Add(StringWithQualityHeaderValue.Parse(acceptCharsetHeader));
            }

            if (requestEncoding != null)
            {
                Encoding reqEncoding = Encoding.GetEncoding(requestEncoding);
                StringContent content = new StringContent("", reqEncoding, "text/plain");
                request.Content = content;
            }

            MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
            foreach (string supportedEncoding in supportedEncodings)
            {
                formatter.SupportedEncodings.Add(Encoding.GetEncoding(supportedEncoding));
            }

            // Act
            Encoding actualEncoding = negotiator.SelectResponseCharacterEncoding(request, formatter);

            // Assert
            if (expectedEncoding == null)
            {
                Assert.Null(actualEncoding);
            }
            else
            {
                Assert.Equal(Encoding.GetEncoding(expectedEncoding), actualEncoding);
            }
        }

        [Theory]
        [TestDataSet(typeof(DefaultContentNegotiatorTests), nameof(MediaTypeWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))]
        public void SortMediaTypeWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator();

            List<MediaTypeWithQualityHeaderValue> unsortedValues =
                new List<MediaTypeWithQualityHeaderValue>(unsorted.Select(u => MediaTypeWithQualityHeaderValue.Parse(u)));

            List<MediaTypeWithQualityHeaderValue> expectedSortedValues =
                new List<MediaTypeWithQualityHeaderValue>(expectedSorted.Select(u => MediaTypeWithQualityHeaderValue.Parse(u)));

            // Act
            IEnumerable<MediaTypeWithQualityHeaderValue> actualSorted = negotiator.SortMediaTypeWithQualityHeaderValuesByQFactor(unsortedValues);

            // Assert
            Assert.True(expectedSortedValues.SequenceEqual(actualSorted));
        }

        public static TheoryData<string[], string[]> MediaTypeWithQualityHeaderValueComparerTestsBeforeAfterSortedValues
        {
            get
            {
                return new TheoryData<string[], string[]>
                {
                    {
                        new string[]
                        {
                            "application/*",
                            "text/plain",
                            "text/plain;q=1.0",
                            "text/plain",
                            "text/plain;q=0",
                            "*/*;q=0.8",
                            "*/*;q=1",
                            "text/*;q=1",
                            "text/plain;q=0.8",
                            "text/*;q=0.8",
                            "text/*;q=0.6",
                            "text/*;q=1.0",
                            "*/*;q=0.4",
                            "text/plain;q=0.6",
                            "text/xml",
                        },
                        new string[]
                        {
                            "text/plain",
                            "text/plain;q=1.0",
                            "text/plain",
                            "text/xml",
                            "application/*",
                            "text/*;q=1",
                            "text/*;q=1.0",
                            "*/*;q=1",
                            "text/plain;q=0.8",
                            "text/*;q=0.8",
                            "*/*;q=0.8",
                            "text/plain;q=0.6",
                            "text/*;q=0.6",
                            "*/*;q=0.4",
                            "text/plain;q=0",
                        }
                    }
                };
            }
        }

        [Theory]
        [TestDataSet(typeof(DefaultContentNegotiatorTests), nameof(StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))]
        public void SortStringWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator();

            List<StringWithQualityHeaderValue> unsortedValues =
                new List<StringWithQualityHeaderValue>(unsorted.Select(u => StringWithQualityHeaderValue.Parse(u)));

            List<StringWithQualityHeaderValue> expectedSortedValues =
                new List<StringWithQualityHeaderValue>(expectedSorted.Select(u => StringWithQualityHeaderValue.Parse(u)));

            // Act
            IEnumerable<StringWithQualityHeaderValue> actualSorted = negotiator.SortStringWithQualityHeaderValuesByQFactor(unsortedValues);

            // Assert
            Assert.True(expectedSortedValues.SequenceEqual(actualSorted));
        }

        public static TheoryData<string[], string[]> StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues
        {
            get
            {
                return new TheoryData<string[], string[]>
                {
                    {
                        new string[]
                        {
                            "text",
                            "text;q=1.0",
                            "text",
                            "text;q=0",
                            "*;q=0.8",
                            "*;q=1",
                            "text;q=0.8",
                            "*;q=0.6",
                            "text;q=1.0",
                            "*;q=0.4",
                            "text;q=0.6",
                        },
                        new string[]
                        {
                            "text",
                            "text;q=1.0",
                            "text",
                            "text;q=1.0",
                            "*;q=1",
                            "text;q=0.8",
                            "*;q=0.8",
                            "text;q=0.6",
                            "*;q=0.6",
                            "*;q=0.4",
                            "text;q=0",
                        }
                    }
                };
            }
        }

        [Theory]
        [MemberData(nameof(UpdateBestMatchData))]
        public void UpdateBestMatch_SelectsCorrectly(MediaTypeFormatterMatch current, MediaTypeFormatterMatch replacement, bool currentWins)
        {
            // Arrange
            MockContentNegotiator negotiator = new MockContentNegotiator();

            // Act
            MediaTypeFormatterMatch actualResult = negotiator.UpdateBestMatch(current, replacement);

            // Assert
            if (currentWins)
            {
                Assert.Same(current, actualResult);
            }
            else
            {
                Assert.Same(replacement, actualResult);
            }
        }

        private class PlainTextFormatter : MediaTypeFormatter
        {
            public override bool CanReadType(Type type)
            {
                return true;
            }

            public override bool CanWriteType(Type type)
            {
                return true;
            }
        }
    }
}
