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

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Formatters
{
    public class TextInputFormatterTest
    {
        [Fact]
        public async Task ReadAsync_ReturnsFailure_IfItCanNotUnderstandTheContentTypeEncoding()
        {
            // Arrange
            var formatter = new TestFormatter();
            formatter.SupportedEncodings.Add(Encoding.ASCII);

            var context = new InputFormatterContext(
                new DefaultHttpContext(),
                "something",
                new ModelStateDictionary(),
                new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
                (stream, encoding) => new StreamReader(stream, encoding));

            context.HttpContext.Request.ContentType = "application/json;charset=utf-8";
            context.HttpContext.Request.ContentLength = 1;

            // Act
            var result = await formatter.ReadAsync(context);

            // Assert
            Assert.True(result.HasError);
            Assert.True(context.ModelState.ContainsKey("something"));
            Assert.Single(context.ModelState["something"].Errors);

            var error = context.ModelState["something"].Errors[0];
            Assert.IsType<UnsupportedContentTypeException>(error.Exception);
        }

        [Fact]
        public void SelectCharacterEncoding_ThrowsInvalidOperationException_IfItDoesNotHaveAValidEncoding()
        {
            // Arrange
            var formatter = new TestFormatter();

            var context = new InputFormatterContext(
                new DefaultHttpContext(),
                "something",
                new ModelStateDictionary(),
                new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
                (stream, encoding) => new StreamReader(stream, encoding));

            context.HttpContext.Request.ContentLength = 1;

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

        [Theory]
        [InlineData("utf-8")]
        [InlineData("invalid")]
        public void SelectCharacterEncoding_ReturnsNull_IfItCanNotUnderstandContentTypeEncoding(string charset)
        {
            // Arrange
            var formatter = new TestFormatter();
            formatter.SupportedEncodings.Add(Encoding.UTF32);

            var context = new InputFormatterContext(
                new DefaultHttpContext(),
                "something",
                new ModelStateDictionary(),
                new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
                (stream, encoding) => new StreamReader(stream, encoding));

            context.HttpContext.Request.ContentType = "application/json;charset=" + charset;

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

            // Assert
            Assert.Null(result);
        }

        [Fact]
        public void SelectCharacterEncoding_ReturnsContentTypeEncoding_IfItCanUnderstandIt()
        {
            // Arrange
            var formatter = new TestFormatter();
            formatter.SupportedEncodings.Add(Encoding.UTF32);
            formatter.SupportedEncodings.Add(Encoding.UTF8);

            var context = new InputFormatterContext(
                new DefaultHttpContext(),
                "something",
                new ModelStateDictionary(),
                new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
                (stream, encoding) => new StreamReader(stream, encoding));

            context.HttpContext.Request.ContentType = "application/json;charset=utf-8";

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

            // Assert
            Assert.Equal(Encoding.UTF8, result);
        }

        [Theory]
        [InlineData("unicode-1-1-utf-8")]
        [InlineData("unicode-2-0-utf-8")]
        public void SelectCharacterEncoding_ReturnsUTF8Encoding_IfContentTypeIsAnAlias(string charset)
        {
            // Arrange
            var formatter = new TestFormatter();
            formatter.SupportedEncodings.Add(Encoding.UTF32);
            formatter.SupportedEncodings.Add(Encoding.UTF8);

            var context = new InputFormatterContext(
                new DefaultHttpContext(),
                "something",
                new ModelStateDictionary(),
                new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
                (stream, encoding) => new StreamReader(stream, encoding));

            context.HttpContext.Request.ContentType = "application/json;charset=" + charset;

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

            // Assert
            Assert.Equal(Encoding.UTF8, result);
        }

        [Theory]
        [InlineData("ANSI_X3.4-1968")]
        [InlineData("ANSI_X3.4-1986")]
        [InlineData("ascii")]
        [InlineData("cp367")]
        [InlineData("csASCII")]
        [InlineData("IBM367")]
        [InlineData("iso-ir-6")]
        [InlineData("ISO646-US")]
        [InlineData("ISO_646.irv:1991")]
        [InlineData("us")]
        public void SelectCharacterEncoding_ReturnsAsciiEncoding_IfContentTypeIsAnAlias(string charset)
        {
            // Arrange
            var formatter = new TestFormatter();
            formatter.SupportedEncodings.Add(Encoding.UTF32);
            formatter.SupportedEncodings.Add(Encoding.ASCII);

            var context = new InputFormatterContext(
                new DefaultHttpContext(),
                "something",
                new ModelStateDictionary(),
                new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
                (stream, encoding) => new StreamReader(stream, encoding));

            context.HttpContext.Request.ContentType = "application/json;charset=\"" + charset + "\"";

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

            // Assert
            Assert.Equal(Encoding.ASCII, result);
        }

        [Theory]
        [InlineData("")]
        [InlineData("(garbage)")]
        [InlineData("(garbage); charset=utf-32")]
        [InlineData("text/(garbage)")]
        [InlineData("text/(garbage); charset=utf-32")]
        [InlineData("application/json")]
        [InlineData("application/json; charset")]
        [InlineData("application/json; charset=(garbage)")]
        [InlineData("application/json; version=(garbage); charset=utf-32")]
        public void SelectCharacterEncoding_ReturnsFirstEncoding_IfContentTypeIsMissingInvalidOrDoesNotHaveEncoding(
            string contentType)
        {
            // Arrange
            var formatter = new TestFormatter();
            formatter.SupportedEncodings.Add(Encoding.UTF8);
            formatter.SupportedEncodings.Add(Encoding.UTF32);

            var context = new InputFormatterContext(
                new DefaultHttpContext(),
                "something",
                new ModelStateDictionary(),
                new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
                (stream, encoding) => new StreamReader(stream, encoding));

            context.HttpContext.Request.ContentType = contentType;

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

            // Assert
            Assert.Equal(Encoding.UTF8, result);
        }

        private class TestFormatter : TextInputFormatter
        {
            private readonly object _object;

            public TestFormatter() : this(null) { }

            public TestFormatter(object @object)
            {
                _object = @object;
            }

            public IList<Type> SupportedTypes { get; } = new List<Type>();

            protected override bool CanReadType(Type type)
            {
                return SupportedTypes.Count == 0 ? true : SupportedTypes.Contains(type);
            }

            public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
            {
                return InputFormatterResult.SuccessAsync(_object);
            }

            public Encoding TestSelectCharacterEncoding(InputFormatterContext context)
            {
                return SelectCharacterEncoding(context);
            }
        }
    }
}
