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

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

public class Http1OutputProducerTests : IDisposable
{
    private readonly MemoryPool<byte> _memoryPool;

    public Http1OutputProducerTests()
    {
        _memoryPool = PinnedBlockMemoryPoolFactory.Create();
    }

    public void Dispose()
    {
        _memoryPool.Dispose();
    }

    [Fact]
    public async Task WritesNoopAfterConnectionCloses()
    {
        var pipeOptions = new PipeOptions
        (
            pool: _memoryPool,
            readerScheduler: Mock.Of<PipeScheduler>(),
            writerScheduler: PipeScheduler.Inline,
            useSynchronizationContext: false
        );

        using (var socketOutput = CreateOutputProducer(pipeOptions))
        {
            // Close
            socketOutput.Dispose();

            await socketOutput.WriteDataAsync(new byte[] { 1, 2, 3, 4 }, default);

            Assert.False(socketOutput.Pipe.Reader.TryRead(out var result));

            socketOutput.Pipe.Writer.Complete();
            socketOutput.Pipe.Reader.Complete();
        }
    }

    [Fact]
    public async Task FlushAsync_OnDisposedSocket_ReturnsResultWithIsCompletedTrue()
    {
        var pipeOptions = new PipeOptions
        (
            pool: _memoryPool,
            readerScheduler: Mock.Of<PipeScheduler>(),
            writerScheduler: PipeScheduler.Inline,
            useSynchronizationContext: false
        );

        var socketOutput = CreateOutputProducer(pipeOptions);

        await socketOutput.WriteDataAsync(new byte[] { 1, 2, 3, 4 }, default);
        var successResult = await socketOutput.FlushAsync();
        Assert.False(successResult.IsCompleted);

        // Close
        socketOutput.Dispose();

        await socketOutput.WriteDataAsync(new byte[] { 1, 2, 3, 4 }, default);
        var completedResult = await socketOutput.FlushAsync();
        Assert.True(completedResult.IsCompleted);

        socketOutput.Pipe.Writer.Complete();
        socketOutput.Pipe.Reader.Complete();
    }

    [Fact]
    public async Task FlushAsync_OnSocketWithCanceledPendingFlush_ReturnsResultWithIsCanceledTrue()
    {
        var pipeOptions = new PipeOptions
        (
            pool: _memoryPool,
            readerScheduler: Mock.Of<PipeScheduler>(),
            writerScheduler: PipeScheduler.Inline,
            useSynchronizationContext: false
        );

        var socketOutput = CreateOutputProducer(pipeOptions);

        await socketOutput.WriteDataAsync(new byte[] { 1, 2, 3, 4 }, default);
        var successResult = await socketOutput.FlushAsync();
        Assert.False(successResult.IsCanceled);

        // Close
        socketOutput.CancelPendingFlush();

        var cancelResult = await socketOutput.WriteDataToPipeAsync(new byte[] { 1, 2, 3, 4 }, default);
        Assert.True(cancelResult.IsCanceled);

        // only one flush should be cancelled
        var goodResult = await socketOutput.WriteDataToPipeAsync(new byte[] { 1, 2, 3, 4 }, default);
        Assert.False(goodResult.IsCanceled);

        socketOutput.Pipe.Writer.Complete();
        socketOutput.Pipe.Reader.Complete();
    }

    [Fact]
    public void AbortsTransportEvenAfterDispose()
    {
        var mockConnectionContext = new Mock<ConnectionContext>();

        var outputProducer = CreateOutputProducer(connectionContext: mockConnectionContext.Object);

        outputProducer.Dispose();

        mockConnectionContext.Verify(f => f.Abort(It.IsAny<ConnectionAbortedException>()), Times.Never());

        outputProducer.Abort(null);

        mockConnectionContext.Verify(f => f.Abort(null), Times.Once());

        outputProducer.Abort(null);

        mockConnectionContext.Verify(f => f.Abort(null), Times.Once());
    }

    [Fact]
    public void AllocatesFakeMemorySmallerThanMaxBufferSize()
    {
        var pipeOptions = new PipeOptions
        (
            pool: _memoryPool,
            readerScheduler: Mock.Of<PipeScheduler>(),
            writerScheduler: PipeScheduler.Inline,
            useSynchronizationContext: false
        );

        using (var socketOutput = CreateOutputProducer(pipeOptions))
        {
            var bufferSize = 1;
            var fakeMemory = socketOutput.GetFakeMemory(bufferSize);

            Assert.True(fakeMemory.Length >= bufferSize);
        }
    }

    [Fact]
    public void AllocatesFakeMemoryBiggerThanMaxBufferSize()
    {
        var pipeOptions = new PipeOptions
        (
            pool: _memoryPool,
            readerScheduler: Mock.Of<PipeScheduler>(),
            writerScheduler: PipeScheduler.Inline,
            useSynchronizationContext: false
        );

        using (var socketOutput = CreateOutputProducer(pipeOptions))
        {
            var bufferSize = _memoryPool.MaxBufferSize * 2;
            var fakeMemory = socketOutput.GetFakeMemory(bufferSize);

            Assert.True(fakeMemory.Length >= bufferSize);
        }
    }

    [Fact]
    public void AllocatesIncreasingFakeMemory()
    {
        var pipeOptions = new PipeOptions
        (
            pool: _memoryPool,
            readerScheduler: Mock.Of<PipeScheduler>(),
            writerScheduler: PipeScheduler.Inline,
            useSynchronizationContext: false
        );

        using (var socketOutput = CreateOutputProducer(pipeOptions))
        {
            var bufferSize1 = 1024;
            var bufferSize2 = 2048;
            var fakeMemory = socketOutput.GetFakeMemory(bufferSize1);
            fakeMemory = socketOutput.GetFakeMemory(bufferSize2);

            Assert.True(fakeMemory.Length >= bufferSize2);
        }
    }

    [Fact]
    public void ReusesFakeMemory()
    {
        var pipeOptions = new PipeOptions
        (
            pool: _memoryPool,
            readerScheduler: Mock.Of<PipeScheduler>(),
            writerScheduler: PipeScheduler.Inline,
            useSynchronizationContext: false
        );

        using (var socketOutput = CreateOutputProducer(pipeOptions))
        {
            var bufferSize = 1024;
            var fakeMemory1 = socketOutput.GetFakeMemory(bufferSize);
            var fakeMemory2 = socketOutput.GetFakeMemory(bufferSize);

            Assert.Equal(fakeMemory1, fakeMemory2);
        }
    }

    private TestHttpOutputProducer CreateOutputProducer(
        PipeOptions pipeOptions = null,
        ConnectionContext connectionContext = null)
    {
        pipeOptions = pipeOptions ?? new PipeOptions();
        connectionContext = connectionContext ?? Mock.Of<ConnectionContext>();

        var pipe = new Pipe(pipeOptions);
        var serviceContext = new TestServiceContext();
        var socketOutput = new TestHttpOutputProducer(
            pipe,
            "0",
            connectionContext,
            _memoryPool,
            serviceContext.Log,
            Mock.Of<ITimeoutControl>(),
            Mock.Of<IHttpMinResponseDataRateFeature>(),
            Mock.Of<IHttpOutputAborter>());

        return socketOutput;
    }

    private class TestHttpOutputProducer : Http1OutputProducer
    {
        public TestHttpOutputProducer(Pipe pipe, string connectionId, ConnectionContext connectionContext, MemoryPool<byte> memoryPool, KestrelTrace log, ITimeoutControl timeoutControl, IHttpMinResponseDataRateFeature minResponseDataRateFeature, IHttpOutputAborter outputAborter)
            : base(pipe.Writer, connectionId, connectionContext, memoryPool, log, timeoutControl, minResponseDataRateFeature, outputAborter)
        {
            Pipe = pipe;
        }

        public Pipe Pipe { get; }
    }
}
