// 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.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO.Pipelines;
using System.Text;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;

public class HttpResponseHeadersTests
{
    [Fact]
    public void InitialDictionaryIsEmpty()
    {
        using (var memoryPool = PinnedBlockMemoryPoolFactory.Create())
        {
            var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
            var pair = DuplexPipe.CreateConnectionPair(options, options);
            var http1ConnectionContext = TestContextFactory.CreateHttpConnectionContext(
                serviceContext: new TestServiceContext(),
                connectionContext: Mock.Of<ConnectionContext>(),
                transport: pair.Transport,
                memoryPool: memoryPool,
                connectionFeatures: new FeatureCollection());

            var http1Connection = new Http1Connection(http1ConnectionContext);

            http1Connection.Reset();

            IDictionary<string, StringValues> headers = http1Connection.ResponseHeaders;

            Assert.Equal(0, headers.Count);
            Assert.False(headers.IsReadOnly);
        }
    }

    [Theory]
    [InlineData("Server", "\r\nData")]
    [InlineData("Server", "\0Data")]
    [InlineData("Server", "Data\r")]
    [InlineData("Server", "Da\0ta")]
    [InlineData("Server", "Da\u001Fta")]
    [InlineData("Unknown-Header", "\r\nData")]
    [InlineData("Unknown-Header", "\0Data")]
    [InlineData("Unknown-Header", "Data\0")]
    [InlineData("Unknown-Header", "Da\nta")]
    [InlineData("\r\nServer", "Data")]
    [InlineData("Server\r", "Data")]
    [InlineData("Ser\0ver", "Data")]
    [InlineData("Server\r\n", "Data")]
    [InlineData("\u0000Server", "Data")]
    [InlineData("Server", "Data\u0000")]
    [InlineData("\u001FServer", "Data")]
    [InlineData("Unknown-Header\r\n", "Data")]
    [InlineData("\0Unknown-Header", "Data")]
    [InlineData("Unknown\r-Header", "Data")]
    [InlineData("Unk\nown-Header", "Data")]
    [InlineData("Server", "Da\u007Fta")]
    [InlineData("Unknown\u007F-Header", "Data")]
    [InlineData("Ser\u0080ver", "Data")]
    [InlineData("Server", "Da\u0080ta")]
    [InlineData("Unknown\u0080-Header", "Data")]
    [InlineData("Ser™ver", "Data")]
    [InlineData("Server", "Da™ta")]
    [InlineData("Unknown™-Header", "Data")]
    [InlineData("šerver", "Data")]
    [InlineData("Server", "Dašta")]
    [InlineData("Unknownš-Header", "Data")]
    [InlineData("Seršver", "Data")]
    [InlineData("Server\"", "Data")]
    [InlineData("Server(", "Data")]
    [InlineData("Server)", "Data")]
    [InlineData("Server,", "Data")]
    [InlineData("Server/", "Data")]
    [InlineData("Server:", "Data")]
    [InlineData("Server;", "Data")]
    [InlineData("Server<", "Data")]
    [InlineData("Server=", "Data")]
    [InlineData("Server>", "Data")]
    [InlineData("Server?", "Data")]
    [InlineData("Server@", "Data")]
    [InlineData("Server[", "Data")]
    [InlineData("Server\\", "Data")]
    [InlineData("Server]", "Data")]
    [InlineData("Server{", "Data")]
    [InlineData("Server}", "Data")]
    [InlineData("", "Data")]
    [InlineData(null, "Data")]
    public void AddingControlOrNonAsciiCharactersToHeadersThrows(string key, string value)
    {
        var responseHeaders = new HttpResponseHeaders();

        Assert.Throws<InvalidOperationException>(() =>
        {
            ((IHeaderDictionary)responseHeaders)[key] = value;
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            ((IHeaderDictionary)responseHeaders)[key] = new StringValues(new[] { "valid", value });
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            ((IDictionary<string, StringValues>)responseHeaders)[key] = value;
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            var kvp = new KeyValuePair<string, StringValues>(key, value);
            ((ICollection<KeyValuePair<string, StringValues>>)responseHeaders).Add(kvp);
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            var kvp = new KeyValuePair<string, StringValues>(key, value);
            ((IDictionary<string, StringValues>)responseHeaders).Add(key, value);
        });
    }

    [Theory]
    [InlineData("\r\nData")]
    [InlineData("\0Data")]
    [InlineData("Data\r")]
    [InlineData("Da\0ta")]
    [InlineData("Da\u001Fta")]
    [InlineData("Data\0")]
    [InlineData("Da\nta")]
    [InlineData("Da\u007Fta")]
    [InlineData("Da\u0080ta")]
    [InlineData("Da™ta")]
    [InlineData("Dašta")]
    public void AddingControlOrNonAsciiCharactersToHeaderPropertyThrows(string value)
    {
        var responseHeaders = (IHeaderDictionary)new HttpResponseHeaders();

        // Known special header
        Assert.Throws<InvalidOperationException>(() =>
        {
            responseHeaders.Allow = value;
        });

        // Unknown header fallback
        Assert.Throws<InvalidOperationException>(() =>
        {
            responseHeaders.Accept = value;
        });
    }

    [Fact]
    public void AddingTabCharactersToHeaderPropertyWorks()
    {
        var responseHeaders = (IHeaderDictionary)new HttpResponseHeaders();

        // Known special header
        responseHeaders.Allow = "Da\tta";

        // Unknown header fallback
        responseHeaders.Accept = "Da\tta";
    }

    [Theory]
    [InlineData("\r\nData")]
    [InlineData("\0Data")]
    [InlineData("Data\r")]
    [InlineData("Da\0ta")]
    [InlineData("Da\u001Fta")]
    [InlineData("Data\0")]
    [InlineData("Da\nta")]
    [InlineData("Da\u007Fta")]
    public void AddingControlCharactersWithCustomEncoderThrows(string value)
    {
        var responseHeaders = new HttpResponseHeaders(_ => Encoding.UTF8);

        // Known special header
        Assert.Throws<InvalidOperationException>(() =>
        {
            ((IHeaderDictionary)responseHeaders).Allow = value;
        });

        // Unknown header fallback
        Assert.Throws<InvalidOperationException>(() =>
        {
            ((IHeaderDictionary)responseHeaders).Accept = value;
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            ((IHeaderDictionary)responseHeaders)["Unknown"] = value;
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            ((IHeaderDictionary)responseHeaders)["Unknown"] = new StringValues(new[] { "valid", value });
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            ((IDictionary<string, StringValues>)responseHeaders)["Unknown"] = value;
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            var kvp = new KeyValuePair<string, StringValues>("Unknown", value);
            ((ICollection<KeyValuePair<string, StringValues>>)responseHeaders).Add(kvp);
        });

        Assert.Throws<InvalidOperationException>(() =>
        {
            var kvp = new KeyValuePair<string, StringValues>("Unknown", value);
            ((IDictionary<string, StringValues>)responseHeaders).Add("Unknown", value);
        });
    }

    [Theory]
    [InlineData("Da\u0080ta")]
    [InlineData("Da™ta")]
    [InlineData("Dašta")]
    public void AddingNonAsciiCharactersWithCustomEncoderWorks(string value)
    {
        var responseHeaders = new HttpResponseHeaders(_ => Encoding.UTF8);

        // Known special header
        ((IHeaderDictionary)responseHeaders).Allow = value;

        // Unknown header fallback
        ((IHeaderDictionary)responseHeaders).Accept = value;

        ((IHeaderDictionary)responseHeaders)["Unknown"] = value;

        ((IHeaderDictionary)responseHeaders)["Unknown"] = new StringValues(new[] { "valid", value });

        ((IDictionary<string, StringValues>)responseHeaders)["Unknown"] = value;

        ((IHeaderDictionary)responseHeaders).Clear();
        var kvp = new KeyValuePair<string, StringValues>("Unknown", value);
        ((ICollection<KeyValuePair<string, StringValues>>)responseHeaders).Add(kvp);

        ((IHeaderDictionary)responseHeaders).Clear();
        kvp = new KeyValuePair<string, StringValues>("Unknown", value);
        ((IDictionary<string, StringValues>)responseHeaders).Add("Unknown", value);
    }

    [Fact]
    public void ThrowsWhenAddingHeaderAfterReadOnlyIsSet()
    {
        var headers = new HttpResponseHeaders();
        headers.SetReadOnly();

        Assert.Throws<InvalidOperationException>(() => ((IDictionary<string, StringValues>)headers).Add("my-header", new[] { "value" }));
    }

    [Fact]
    public void ThrowsWhenSettingContentLengthPropertyAfterReadOnlyIsSet()
    {
        var headers = new HttpResponseHeaders();
        headers.SetReadOnly();

        Assert.Throws<InvalidOperationException>(() => headers.ContentLength = null);
    }

    [Fact]
    public void ThrowsWhenChangingHeaderAfterReadOnlyIsSet()
    {
        var headers = new HttpResponseHeaders();
        var dictionary = (IDictionary<string, StringValues>)headers;
        dictionary.Add("my-header", new[] { "value" });
        headers.SetReadOnly();

        Assert.Throws<InvalidOperationException>(() => dictionary["my-header"] = "other-value");
    }

    [Fact]
    public void ThrowsWhenRemovingHeaderAfterReadOnlyIsSet()
    {
        var headers = new HttpResponseHeaders();
        var dictionary = (IDictionary<string, StringValues>)headers;
        dictionary.Add("my-header", new[] { "value" });
        headers.SetReadOnly();

        Assert.Throws<InvalidOperationException>(() => dictionary.Remove("my-header"));
    }

    [Fact]
    public void ThrowsWhenClearingHeadersAfterReadOnlyIsSet()
    {
        var headers = new HttpResponseHeaders();
        var dictionary = (IDictionary<string, StringValues>)headers;
        dictionary.Add("my-header", new[] { "value" });
        headers.SetReadOnly();

        Assert.Throws<InvalidOperationException>(() => dictionary.Clear());
    }

    [Theory]
    [MemberData(nameof(BadContentLengths))]
    public void ThrowsWhenAddingContentLengthWithNonNumericValue(string contentLength)
    {
        var headers = new HttpResponseHeaders();
        var dictionary = (IDictionary<string, StringValues>)headers;

        var exception = Assert.Throws<InvalidOperationException>(() => dictionary.Add("Content-Length", new[] { contentLength }));
        Assert.Equal(CoreStrings.FormatInvalidContentLength_InvalidNumber(contentLength), exception.Message);
    }

    [Theory]
    [MemberData(nameof(BadContentLengths))]
    public void ThrowsWhenSettingContentLengthToNonNumericValue(string contentLength)
    {
        var headers = new HttpResponseHeaders();
        var dictionary = (IDictionary<string, StringValues>)headers;

        var exception = Assert.Throws<InvalidOperationException>(() => ((IHeaderDictionary)headers)["Content-Length"] = contentLength);
        Assert.Equal(CoreStrings.FormatInvalidContentLength_InvalidNumber(contentLength), exception.Message);
    }

    [Theory]
    [MemberData(nameof(BadContentLengths))]
    public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue(string contentLength)
    {
        var headers = new HttpResponseHeaders();

        var exception = Assert.Throws<InvalidOperationException>(() => headers.HeaderContentLength = contentLength);
        Assert.Equal(CoreStrings.FormatInvalidContentLength_InvalidNumber(contentLength), exception.Message);
    }

    [Theory]
    [MemberData(nameof(GoodContentLengths))]
    public void ContentLengthValueCanBeReadAsLongAfterAddingHeader(string contentLength)
    {
        var headers = new HttpResponseHeaders();
        var dictionary = (IDictionary<string, StringValues>)headers;
        dictionary.Add("Content-Length", contentLength);

        Assert.Equal(ParseLong(contentLength), headers.ContentLength);
    }

    [Theory]
    [MemberData(nameof(GoodContentLengths))]
    public void ContentLengthValueCanBeReadAsLongAfterSettingHeader(string contentLength)
    {
        var headers = new HttpResponseHeaders();
        var dictionary = (IDictionary<string, StringValues>)headers;
        dictionary["Content-Length"] = contentLength;

        Assert.Equal(ParseLong(contentLength), headers.ContentLength);
    }

    [Theory]
    [MemberData(nameof(GoodContentLengths))]
    public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader(string contentLength)
    {
        var headers = new HttpResponseHeaders();
        headers.HeaderContentLength = contentLength;

        Assert.Equal(ParseLong(contentLength), headers.ContentLength);
    }

    [Fact]
    public void ContentLengthValueClearedWhenHeaderIsRemoved()
    {
        var headers = new HttpResponseHeaders();
        headers.HeaderContentLength = "42";
        var dictionary = (IDictionary<string, StringValues>)headers;

        dictionary.Remove("Content-Length");

        Assert.Null(headers.ContentLength);
    }

    [Fact]
    public void ContentLengthValueClearedWhenHeadersCleared()
    {
        var headers = new HttpResponseHeaders();
        headers.HeaderContentLength = "42";
        var dictionary = (IDictionary<string, StringValues>)headers;

        dictionary.Clear();

        Assert.Null(headers.ContentLength);
    }

    private static long ParseLong(string value)
    {
        return long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture);
    }

    public static TheoryData<string> GoodContentLengths => new TheoryData<string>
        {
            "0",
            "00",
            "042",
            "42",
            long.MaxValue.ToString(CultureInfo.InvariantCulture)
        };

    public static TheoryData<string> BadContentLengths => new TheoryData<string>
        {
            "",
            " ",
            " 42",
            "42 ",
            "bad",
            "!",
            "!42",
            "42!",
            "42,000",
            "42.000",
        };
}
