// 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.Buffers;
using System.IO.Pipelines;
using System.Text;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
    public class StartLineTests : IDisposable
    {
        private readonly static IKestrelTrace _trace = Mock.Of<IKestrelTrace>();

        private IDuplexPipe Transport { get; }
        private MemoryPool<byte> MemoryPool { get; }
        private Http1Connection Http1Connection { get; }
        private Http1ParsingHandler ParsingHandler {get;}
        private IHttpParser<Http1ParsingHandler> Parser { get; }

        [Fact]
        public void InOriginForm()
        {
            var rawTarget = "/path%20with%20spaces?q=123&w=xyzw1";
            var path = "/path with spaces";
            var query = "?q=123&w=xyzw1";
            Http1Connection.Reset();
            // RawTarget, Path, QueryString are null after reset
            Assert.Null(Http1Connection.RawTarget);
            Assert.Null(Http1Connection.Path);
            Assert.Null(Http1Connection.QueryString);

            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"POST {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // But not the same as the inputs.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            Assert.NotSame(path, Http1Connection.Path);
            Assert.NotSame(query, Http1Connection.QueryString);
        }

        [Fact]
        public void InAuthorityForm()
        {
            var rawTarget = "example.com:1234";
            var path = string.Empty;
            var query = string.Empty;
            Http1Connection.Reset();
            // RawTarget, Path, QueryString are null after reset
            Assert.Null(Http1Connection.RawTarget);
            Assert.Null(Http1Connection.Path);
            Assert.Null(Http1Connection.QueryString);

            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // But not the same as the inputs.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            // Empty strings, so interned and the same.
            Assert.Same(path, Http1Connection.Path);
            Assert.Same(query, Http1Connection.QueryString);
        }

        [Fact]
        public void InAbsoluteForm()
        {
            var rawTarget = "http://localhost/path1?q=123&w=xyzw";
            var path = "/path1";
            var query = "?q=123&w=xyzw";
            Http1Connection.Reset();
            // RawTarget, Path, QueryString are null after reset
            Assert.Null(Http1Connection.RawTarget);
            Assert.Null(Http1Connection.Path);
            Assert.Null(Http1Connection.QueryString);

            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // But not the same as the inputs.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            Assert.NotSame(path, Http1Connection.Path);
            Assert.NotSame(query, Http1Connection.QueryString);
        }

        [Fact]
        public void InAsteriskForm()
        {
            var rawTarget = "*";
            var path = string.Empty;
            var query = string.Empty;
            Http1Connection.Reset();
            // RawTarget, Path, QueryString are null after reset
            Assert.Null(Http1Connection.RawTarget);
            Assert.Null(Http1Connection.Path);
            Assert.Null(Http1Connection.QueryString);

            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // Asterisk is interned string, so the same.
            Assert.Same(rawTarget, Http1Connection.RawTarget);
            // Empty strings, so interned and the same.
            Assert.Same(path, Http1Connection.Path);
            Assert.Same(query, Http1Connection.QueryString);
        }

        [Fact]
        public void DifferentFormsWorkTogether()
        {
            // InOriginForm
            var rawTarget = "/a%20path%20with%20spaces?q=123&w=xyzw12";
            var path = "/a path with spaces";
            var query = "?q=123&w=xyzw12";
            Http1Connection.Reset();
            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"POST {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // But not the same as the inputs.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            Assert.NotSame(path, Http1Connection.Path);
            Assert.NotSame(query, Http1Connection.QueryString);

            InAuthorityForm();

            InOriginForm();
            InAbsoluteForm();

            InOriginForm();
            InAsteriskForm();

            InAuthorityForm();
            InAsteriskForm();

            InAbsoluteForm();
            InAuthorityForm();

            InAbsoluteForm();
            InAsteriskForm();

            InAbsoluteForm();
            InAuthorityForm();
        }

        [Theory]
        [InlineData("/abs/path", "/abs/path", "")]
        [InlineData("/", "/", "")]
        [InlineData("/path", "/path", "")]
        [InlineData("/?q=123&w=xyz", "/", "?q=123&w=xyz")]
        [InlineData("/path?q=123&w=xyz", "/path", "?q=123&w=xyz")]
        [InlineData("/path%20with%20space?q=abc%20123", "/path with space", "?q=abc%20123")]
        public void OriginForms(string rawTarget, string path, string query)
        {
            Http1Connection.Reset();
            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            var prevRequestUrl = Http1Connection.RawTarget;
            var prevPath = Http1Connection.Path;
            var prevQuery = Http1Connection.QueryString;

            // Identical requests keep same materialized string values
            for (var i = 0; i < 5; i++)
            {
                Http1Connection.Reset();
                // RawTarget, Path, QueryString are null after reset
                Assert.Null(Http1Connection.RawTarget);
                Assert.Null(Http1Connection.Path);
                Assert.Null(Http1Connection.QueryString);

                // Parser decodes % encoding in place, so we need to recreate the ROS
                ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
                Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

                // Equal the inputs.
                Assert.Equal(rawTarget, Http1Connection.RawTarget);
                Assert.Equal(path, Http1Connection.Path);
                Assert.Equal(query, Http1Connection.QueryString);

                // But not the same as the inputs.

                Assert.NotSame(rawTarget, Http1Connection.RawTarget);
                Assert.NotSame(path, Http1Connection.Path);
                // string.Empty is used for empty strings, so should be the same.
                Assert.True(query.Length == 0 || !ReferenceEquals(query, Http1Connection.QueryString));

                // However, materalized strings are reused if generated for previous requests.

                Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
                Assert.Same(prevPath, Http1Connection.Path);
                Assert.Same(prevQuery, Http1Connection.QueryString);

                prevRequestUrl = Http1Connection.RawTarget;
                prevPath = Http1Connection.Path;
                prevQuery = Http1Connection.QueryString;
            }

            // Different OriginForm request changes values

            rawTarget = "/path1?q=123&w=xyzw";
            path = "/path1";
            query = "?q=123&w=xyzw";

            Http1Connection.Reset();
            ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
            Parser.ParseRequestLine(ParsingHandler, ros, out _, out _);

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // But not the same as the inputs.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            Assert.NotSame(path, Http1Connection.Path);
            Assert.NotSame(query, Http1Connection.QueryString);

            // Not equal previous request.
            Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
            Assert.NotEqual(prevPath, Http1Connection.Path);
            Assert.NotEqual(prevQuery, Http1Connection.QueryString);

            DifferentFormsWorkTogether();
        }

        [Theory]
        [InlineData("http://localhost/abs/path", "/abs/path", "")]
        [InlineData("https://localhost/abs/path", "/abs/path", "")] // handles mismatch scheme
        [InlineData("https://localhost:22/abs/path", "/abs/path", "")] // handles mismatched ports
        [InlineData("https://differenthost/abs/path", "/abs/path", "")] // handles mismatched hostname
        [InlineData("http://localhost/", "/", "")]
        [InlineData("http://root@contoso.com/path", "/path", "")]
        [InlineData("http://root:password@contoso.com/path", "/path", "")]
        [InlineData("https://localhost/", "/", "")]
        [InlineData("http://localhost", "/", "")]
        [InlineData("http://127.0.0.1/", "/", "")]
        [InlineData("http://[::1]/", "/", "")]
        [InlineData("http://[::1]:8080/", "/", "")]
        [InlineData("http://localhost?q=123&w=xyz", "/", "?q=123&w=xyz")]
        [InlineData("http://localhost/?q=123&w=xyz", "/", "?q=123&w=xyz")]
        [InlineData("http://localhost/path?q=123&w=xyz", "/path", "?q=123&w=xyz")]
        [InlineData("http://localhost/path%20with%20space?q=abc%20123", "/path with space", "?q=abc%20123")]
        public void AbsoluteForms(string rawTarget, string path, string query)
        {
            Http1Connection.Reset();
            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            var prevRequestUrl = Http1Connection.RawTarget;
            var prevPath = Http1Connection.Path;
            var prevQuery = Http1Connection.QueryString;

            // Identical requests keep same materialized string values
            for (var i = 0; i < 5; i++)
            {
                Http1Connection.Reset();
                // RawTarget, Path, QueryString are null after reset
                Assert.Null(Http1Connection.RawTarget);
                Assert.Null(Http1Connection.Path);
                Assert.Null(Http1Connection.QueryString);

                ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
                Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

                // Equal the inputs.
                Assert.Equal(rawTarget, Http1Connection.RawTarget);
                Assert.Equal(path, Http1Connection.Path);
                Assert.Equal(query, Http1Connection.QueryString);

                // But not the same as the inputs.

                Assert.NotSame(rawTarget, Http1Connection.RawTarget);
                Assert.NotSame(path, Http1Connection.Path);
                // string.Empty is used for empty strings, so should be the same.
                Assert.True(query.Length == 0 || !ReferenceEquals(query, Http1Connection.QueryString));

                // However, materalized strings are reused if generated for previous requests.

                Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
                Assert.Same(prevPath, Http1Connection.Path);
                Assert.Same(prevQuery, Http1Connection.QueryString);

                prevRequestUrl = Http1Connection.RawTarget;
                prevPath = Http1Connection.Path;
                prevQuery = Http1Connection.QueryString;
            }

            // Different Absolute Form request changes values

            rawTarget = "http://localhost/path1?q=123&w=xyzw";
            path = "/path1";
            query = "?q=123&w=xyzw";

            Http1Connection.Reset();
            ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
            Parser.ParseRequestLine(ParsingHandler, ros, out _, out _);

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // But not the same as the inputs.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            Assert.NotSame(path, Http1Connection.Path);
            Assert.NotSame(query, Http1Connection.QueryString);

            // Not equal previous request.
            Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
            Assert.NotEqual(prevPath, Http1Connection.Path);
            Assert.NotEqual(prevQuery, Http1Connection.QueryString);

            DifferentFormsWorkTogether();
        }

        [Fact]
        public void AsteriskForms()
        {
            var rawTarget = "*";
            var path = string.Empty;
            var query = string.Empty;

            Http1Connection.Reset();
            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            var prevRequestUrl = Http1Connection.RawTarget;
            var prevPath = Http1Connection.Path;
            var prevQuery = Http1Connection.QueryString;

            // Identical requests keep same materialized string values
            for (var i = 0; i < 5; i++)
            {
                Http1Connection.Reset();
                // RawTarget, Path, QueryString are null after reset
                Assert.Null(Http1Connection.RawTarget);
                Assert.Null(Http1Connection.Path);
                Assert.Null(Http1Connection.QueryString);

                ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
                Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

                // Equal the inputs.
                Assert.Equal(rawTarget, Http1Connection.RawTarget);
                Assert.Equal(path, Http1Connection.Path);
                Assert.Equal(query, Http1Connection.QueryString);

                // Also same as the inputs (interned strings).

                Assert.Same(rawTarget, Http1Connection.RawTarget);
                Assert.Same(path, Http1Connection.Path);
                Assert.Same(query, Http1Connection.QueryString);

                // Materalized strings are reused if generated for previous requests.

                Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
                Assert.Same(prevPath, Http1Connection.Path);
                Assert.Same(prevQuery, Http1Connection.QueryString);

                prevRequestUrl = Http1Connection.RawTarget;
                prevPath = Http1Connection.Path;
                prevQuery = Http1Connection.QueryString;
            }

            // Different request changes values (can't be Astrisk Form as all the same)
            rawTarget = "http://localhost/path1?q=123&w=xyzw";
            path = "/path1";
            query = "?q=123&w=xyzw";

            Http1Connection.Reset();
            ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
            Parser.ParseRequestLine(ParsingHandler, ros, out _, out _);

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // But not the same as the inputs.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            Assert.NotSame(path, Http1Connection.Path);
            Assert.NotSame(query, Http1Connection.QueryString);

            // Not equal previous request.
            Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
            Assert.NotEqual(prevPath, Http1Connection.Path);
            Assert.NotEqual(prevQuery, Http1Connection.QueryString);

            DifferentFormsWorkTogether();
        }

        [Theory]
        [InlineData("localhost", "", "")]
        [InlineData("localhost:22", "", "")] // handles mismatched ports
        [InlineData("differenthost", "", "")] // handles mismatched hostname
        [InlineData("127.0.0.1", "", "")]
        [InlineData("[::1]", "", "")]
        [InlineData("[::1]:8080", "", "")]
        public void AuthorityForms(string rawTarget, string path, string query)
        {
            Http1Connection.Reset();
            var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

            var prevRequestUrl = Http1Connection.RawTarget;
            var prevPath = Http1Connection.Path;
            var prevQuery = Http1Connection.QueryString;

            // Identical requests keep same materialized string values
            for (var i = 0; i < 5; i++)
            {
                Http1Connection.Reset();
                // RawTarget, Path, QueryString are null after reset
                Assert.Null(Http1Connection.RawTarget);
                Assert.Null(Http1Connection.Path);
                Assert.Null(Http1Connection.QueryString);

                ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
                Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));

                // Equal the inputs.
                Assert.Equal(rawTarget, Http1Connection.RawTarget);
                Assert.Equal(path, Http1Connection.Path);
                Assert.Equal(query, Http1Connection.QueryString);

                // RawTarget not the same as the input.
                Assert.NotSame(rawTarget, Http1Connection.RawTarget);
                // Others same as the inputs, empty strings.
                Assert.Same(path, Http1Connection.Path);
                Assert.Same(query, Http1Connection.QueryString);

                // However, materalized strings are reused if generated for previous requests.

                Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
                Assert.Same(prevPath, Http1Connection.Path);
                Assert.Same(prevQuery, Http1Connection.QueryString);

                prevRequestUrl = Http1Connection.RawTarget;
                prevPath = Http1Connection.Path;
                prevQuery = Http1Connection.QueryString;
            }

            // Different Authority Form request changes values
            rawTarget = "example.org:2345";
            path = "";
            query = "";

            Http1Connection.Reset();
            ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
            Parser.ParseRequestLine(ParsingHandler, ros, out _, out _);

            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);

            // But not the same as the inputs.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            // Empty interned strings
            Assert.Same(path, Http1Connection.Path);
            Assert.Same(query, Http1Connection.QueryString);

            // Not equal previous request.
            Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);

            DifferentFormsWorkTogether();
        }

        public StartLineTests()
        {
            MemoryPool = SlabMemoryPoolFactory.Create();
            var options = new PipeOptions(MemoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
            var pair = DuplexPipe.CreateConnectionPair(options, options);
            Transport = pair.Transport;

            var serviceContext = new ServiceContext
            {
                ServerOptions = new KestrelServerOptions(),
                Log = _trace,
                HttpParser = new HttpParser<Http1ParsingHandler>()
            };

            Http1Connection = new Http1Connection(context: new HttpConnectionContext
            {
                ServiceContext = serviceContext,
                ConnectionFeatures = new FeatureCollection(),
                MemoryPool = MemoryPool,
                Transport = Transport,
                TimeoutControl = new TimeoutControl(timeoutHandler: null)
            });

            Parser = new HttpParser<Http1ParsingHandler>(showErrorDetails: true);
            ParsingHandler = new Http1ParsingHandler(Http1Connection);
        }

        public void Dispose()
        {
            Transport.Input.Complete();
            Transport.Output.Complete();

            MemoryPool.Dispose();
        }
    }
}
