// 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 };
            });
        }
    }
}
