// 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.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
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.Core.Tests
{
    public class BodyControlTests
    {
        [Fact]
        public async Task BodyControlThrowAfterAbort()
        {
            var bodyControl = new BodyControl(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());
            var (request, response, requestPipe, responsePipe) = bodyControl.Start(new MockMessageBody());

            var ex = new Exception("My error");
            bodyControl.Abort(ex);

            await response.WriteAsync(new byte[1], 0, 1);
            Assert.Same(ex,
                await Assert.ThrowsAsync<Exception>(() => request.ReadAsync(new byte[1], 0, 1)));
            Assert.Same(ex,
                await Assert.ThrowsAsync<Exception>(async () => await requestPipe.ReadAsync()));
        }

        [Fact]
        public async Task BodyControlThrowOnAbortAfterUpgrade()
        {
            var bodyControl = new BodyControl(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());
            var (request, response, requestPipe, responsePipe) = bodyControl.Start(new MockMessageBody(upgradeable: true));

            var upgrade = bodyControl.Upgrade();
            var ex = new Exception("My error");
            bodyControl.Abort(ex);

            var writeEx = await Assert.ThrowsAsync<InvalidOperationException>(() => response.WriteAsync(new byte[1], 0, 1));
            Assert.Equal(CoreStrings.ResponseStreamWasUpgraded, writeEx.Message);

            Assert.Same(ex,
                await Assert.ThrowsAsync<Exception>(() => request.ReadAsync(new byte[1], 0, 1)));

            Assert.Same(ex,
                await Assert.ThrowsAsync<Exception>(() => upgrade.ReadAsync(new byte[1], 0, 1)));

            Assert.Same(ex,
                await Assert.ThrowsAsync<Exception>(async () => await requestPipe.ReadAsync()));

            await upgrade.WriteAsync(new byte[1], 0, 1);
        }

        [Fact]
        public async Task BodyControlThrowOnUpgradeAfterAbort()
        {
            var bodyControl = new BodyControl(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());

            var (request, response, requestPipe, responsePipe) = bodyControl.Start(new MockMessageBody(upgradeable: true));
            var ex = new Exception("My error");
            bodyControl.Abort(ex);

            var upgrade = bodyControl.Upgrade();

            var writeEx = await Assert.ThrowsAsync<InvalidOperationException>(() => response.WriteAsync(new byte[1], 0, 1));
            Assert.Equal(CoreStrings.ResponseStreamWasUpgraded, writeEx.Message);

            Assert.Same(ex,
                await Assert.ThrowsAsync<Exception>(() => request.ReadAsync(new byte[1], 0, 1)));

            Assert.Same(ex,
                await Assert.ThrowsAsync<Exception>(() => upgrade.ReadAsync(new byte[1], 0, 1)));
            Assert.Same(ex,
                await Assert.ThrowsAsync<Exception>(async () => await requestPipe.ReadAsync()));

            await upgrade.WriteAsync(new byte[1], 0, 1);
        }

        [Fact]
        public async Task RequestPipeMethodsThrowAfterAbort()
        {
            var bodyControl = new BodyControl(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());

            var (_, response, requestPipe, responsePipe) = bodyControl.Start(new MockMessageBody(upgradeable: true));
            var ex = new Exception("My error");
            bodyControl.Abort(ex);

            await response.WriteAsync(new byte[1], 0, 1);
            Assert.Same(ex,
                Assert.Throws<Exception>(() => requestPipe.AdvanceTo(new SequencePosition())));
            Assert.Same(ex,
                Assert.Throws<Exception>(() => requestPipe.AdvanceTo(new SequencePosition(), new SequencePosition())));
            Assert.Same(ex,
                Assert.Throws<Exception>(() => requestPipe.CancelPendingRead()));
            Assert.Same(ex,
                Assert.Throws<Exception>(() => requestPipe.TryRead(out var res)));
            Assert.Same(ex,
                Assert.Throws<Exception>(() => requestPipe.Complete()));
        }

        [Fact]
        public async Task RequestPipeThrowsObjectDisposedExceptionAfterStop()
        {
            var bodyControl = new BodyControl(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());

            var (_, response, requestPipe, responsePipe) = bodyControl.Start(new MockMessageBody());

            await bodyControl.StopAsync();

            Assert.Throws<ObjectDisposedException>(() => requestPipe.AdvanceTo(new SequencePosition()));
            Assert.Throws<ObjectDisposedException>(() => requestPipe.AdvanceTo(new SequencePosition(), new SequencePosition()));
            Assert.Throws<ObjectDisposedException>(() => requestPipe.CancelPendingRead());
            Assert.Throws<ObjectDisposedException>(() => requestPipe.TryRead(out var res));
            Assert.Throws<ObjectDisposedException>(() => requestPipe.Complete());
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await requestPipe.ReadAsync());
        }

        [Fact]
        public async Task ResponsePipeThrowsObjectDisposedExceptionAfterStop()
        {
            var bodyControl = new BodyControl(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());

            var (_, response, requestPipe, responsePipe) = bodyControl.Start(new MockMessageBody());

            await bodyControl.StopAsync();

            Assert.Throws<ObjectDisposedException>(() => responsePipe.Advance(1));
            Assert.Throws<ObjectDisposedException>(() => responsePipe.CancelPendingFlush());
            Assert.Throws<ObjectDisposedException>(() => responsePipe.GetMemory());
            Assert.Throws<ObjectDisposedException>(() => responsePipe.GetSpan());
            Assert.Throws<ObjectDisposedException>(() => responsePipe.Complete());
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await responsePipe.WriteAsync(new Memory<byte>()));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await responsePipe.FlushAsync());
        }

        private class MockMessageBody : MessageBody
        {
            public MockMessageBody(bool upgradeable = false)
                : base(null)
            {
                RequestUpgrade = upgradeable;
            }

            public override void AdvanceTo(SequencePosition consumed)
            {
                throw new NotImplementedException();
            }

            public override void AdvanceTo(SequencePosition consumed, SequencePosition examined)
            {
                throw new NotImplementedException();
            }

            public override void CancelPendingRead()
            {
                throw new NotImplementedException();
            }

            public override void Complete(Exception exception)
            {
                throw new NotImplementedException();
            }

            public override ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)
            {
                throw new NotImplementedException();
            }

            public override bool TryRead(out ReadResult readResult)
            {
                throw new NotImplementedException();
            }
        }
    }
}
