// 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.Linq;
using System.Text;
using Microsoft.AspNetCore.Internal;
using Xunit;

namespace Microsoft.AspNetCore.Shared.Tests
{
    public class UrlDecoderTests
    {
        [Theory]
        [MemberData(nameof(PathTestData))]
        public void StringDecodeRequestLine(string input, string expected)
        {
            var destination = new char[input.Length];
            int length = UrlDecoder.DecodeRequestLine(input.AsSpan(), destination.AsSpan());
            Assert.True(destination.AsSpan(0, length).SequenceEqual(expected.AsSpan()));
        }

        [Theory]
        [MemberData(nameof(UriTestData))]
        public void ByteDecodeRequestLine(byte[] input, byte[] expected)
        {
            var destination = new byte[input.Length];
            int length = UrlDecoder.DecodeRequestLine(input.AsSpan(), destination.AsSpan(), false);
            Assert.True(destination.AsSpan(0, length).SequenceEqual(expected.AsSpan()));
        }

        [Theory]
        [MemberData(nameof(PathTestData))]
        public void StringDecodeInPlace(string input, string expected)
        {
            var destination = new char[input.Length];
            input.CopyTo(destination);
            int length = UrlDecoder.DecodeInPlace(destination.AsSpan());
            Assert.True(destination.AsSpan(0, length).SequenceEqual(expected.AsSpan()));
        }

        [Theory]
        [MemberData(nameof(UriTestData))]
        public void ByteDecodeInPlace(byte[] input, byte[] expected)
        {
            var destination = new byte[input.Length];
            input.AsSpan().CopyTo(destination);
            int length = UrlDecoder.DecodeInPlace(destination.AsSpan(), false);
            Assert.True(destination.AsSpan(0, length).SequenceEqual(expected.AsSpan()));
        }

        [Fact]
        public void StringDestinationShorterThanSourceDecodeRequestLineThrows()
        {
            var source = new char[2];
            Assert.Throws<ArgumentException>(() => UrlDecoder.DecodeRequestLine(source.AsSpan(), source.AsSpan(0, 1)));
        }

        [Fact]
        public void ByteDestinationShorterThanSourceDecodeRequestLineThrows()
        {
            var source = new byte[2];
            Assert.Throws<ArgumentException>(() => UrlDecoder.DecodeRequestLine(source.AsSpan(), source.AsSpan(0, 1), false));
        }

        [Fact]
        public void StringDestinationLargerThanSourceDecodeRequestLineReturnsCorrenctLenght()
        {
            var source = "/a%20b".ToCharArray();
            var length = UrlDecoder.DecodeRequestLine(source.AsSpan(), new char[source.Length + 10]);
            Assert.Equal(4, length);
        }

        [Fact]
        public void ByteDestinationLargerThanSourceDecodeRequestLineReturnsCorrenctLenght()
        {
            var source = Encoding.UTF8.GetBytes("/a%20b".ToCharArray());
            var length = UrlDecoder.DecodeRequestLine(source.AsSpan(), new byte[source.Length + 10], false);
            Assert.Equal(4, length);
        }

        [Fact]
        public void StringInputNullCharDecodeInPlaceThrows()
        {
            var source = "%00".ToCharArray();
            Assert.Throws<InvalidOperationException>(() => UrlDecoder.DecodeInPlace(source.AsSpan()));
        }

        [Fact]
        public void ByteInputNullCharDecodeInPlaceThrows()
        {
            var source = Encoding.UTF8.GetBytes("%00");
            Assert.Throws<InvalidOperationException>(() => UrlDecoder.DecodeInPlace(source.AsSpan(), false));
        }

        [Theory]
        [InlineData("%$$")]
        [InlineData("%1")]
        [InlineData("%1$")]
        [InlineData("%%1")]
        [InlineData("%%1$")]
        public void StringInputNonHexDecodeInPlaceLeavesUnencoded(string input)
        {
            var source = input.ToCharArray();
            var length = UrlDecoder.DecodeInPlace(source.AsSpan());
            Assert.Equal(input.Length, length);
            Assert.True(source.AsSpan(0, length).SequenceEqual(input.AsSpan()));
        }

        [Theory]
        [InlineData("%$$")]
        [InlineData("%1")]
        [InlineData("%1$")]
        [InlineData("%%1")]
        [InlineData("%%1$")]
        public void ByteInputNonHexDecodeInPlaceLeavesUnencoded(string input)
        {
            var source = Encoding.UTF8.GetBytes(input.ToCharArray());
            var length = UrlDecoder.DecodeInPlace(source.AsSpan(), false);
            Assert.Equal(source.Length, length);
            Assert.True(source.AsSpan(0, length).SequenceEqual(Encoding.UTF8.GetBytes(input).AsSpan()));
        }

        [Theory]
        [InlineData("%2F")]
        public void ByteFormsEncodingDecodeInPlaceDecodesPercent2F(string input)
        {
            var source = Encoding.UTF8.GetBytes(input.ToCharArray());
            var length = UrlDecoder.DecodeInPlace(source.AsSpan(), true);
            Assert.Equal(1, length);
            Assert.True(source.AsSpan(0, length).SequenceEqual(Encoding.UTF8.GetBytes("/").AsSpan()));
        }

        [Theory]
        [InlineData("%FF%FF%FF%FF")] // FF invalid first byte
        [InlineData("%F7%BF%BF%BF")] // beyond 0x10FFFF
        [InlineData("%F7%C0")] // Following byte does not start with 10xx xxxx
        [InlineData("%F0%81")] // Not enough bytes
        [InlineData("%ED%A0%81")] // Invalid range 0xD800-0xDFFF
        public void StringOutOfUtf8RangeDecodeInPlaceLeavesUnencoded(string input)
        {
            var source = input.ToCharArray();
            var length = UrlDecoder.DecodeInPlace(source.AsSpan());
            Assert.Equal(input.Length, length);
            Assert.True(source.AsSpan(0, length).SequenceEqual(input.AsSpan()));
        }

        [Theory]
        [InlineData("%FF%FF%FF%FF")] // FF invalid first byte
        [InlineData("%F7%BF%BF%BF")] // beyond 0x10FFFF
        [InlineData("%F7%C0")] // Following byte does not start with 10xx xxxx
        [InlineData("%F0%81")] // Not enough bytes
        [InlineData("%ED%A0%81")] // Invalid range 0xD800-0xDFFF
        public void ByteOutOfUtf8RangeDecodeInPlaceLeavesUnencoded(string input)
        {
            var source = Encoding.UTF8.GetBytes(input.ToCharArray());
            var length = UrlDecoder.DecodeInPlace(source.AsSpan(), true);
            Assert.Equal(source.Length, length);
            Assert.True(source.AsSpan(0, length).SequenceEqual(Encoding.UTF8.GetBytes(input).AsSpan()));
        }

        public static IEnumerable<object[]> PathTestData
        {
            get
            {
                return new List<object[]>()
                {
                    new[] { "hello", "hello" },
                    new[] { "/", "/" },
                    new[] { "http://localhost:5000/api", "http://localhost:5000/api" },
                    new[] { "/api/abc", "/api/abc" },
                    new[] { "/api/a%2Fb", "/api/a%2Fb" },
                    new[] { "/a%20b", "/a b" },
                    new[] { "/a%24b", "/a$b" },
                    new[] { "/a%C2%A2b", "/a¢b" },
                    new[] { "/a%E0%A4%B9b", "/aहb" },
                    new[] { "/a%E2%82%ACb", "/a€b" },
                    new[] { "/a%ED%95%9Cb", "/a한b" },
                    new[] { "/a%F0%90%8D%88b", "/a𐍈b" },
                    new[] { "/a%25b", "/a%b" },
                    new[] { "/%E4%BD%A0%E5%A5%BD", "/你好" },
                    new[] { "/a%%2Fb", "/a%%2Fb" },
                    new[] { "/a%2Fb+c", "/a%2Fb+c" },
                    new[] { "/%C3%C3%A1", "/%C3á" },
                    new[] { "/a%20%%b", "/a %%b" },
                };
            }
        }

        public static IEnumerable<object[]> UriTestData
        {
            get
            {
                return PathTestData.Select(x =>
                {
                    var input = Encoding.UTF8.GetBytes((string)x[0]);
                    var expected = Encoding.UTF8.GetBytes((string)x[1]);
                    return new[] { input, expected };
                });
            }
        }
    }
}
