// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Buffers;
using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.HighPerformance.Buffers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace CommunityToolkit.HighPerformance.UnitTests.Streams;

[TestClass]
public class Test_IBufferWriterStream
{
    [TestCategory("IBufferWriterStream")]
    [TestMethod]
    public void Test_IBufferWriterStream_Lifecycle()
    {
        ArrayPoolBufferWriter<byte> writer = new();

        // Get a stream from a buffer writer aand validate that it can only be written to.
        // This is to mirror the same functionality as the IBufferWriter<T> interface.
        Stream stream = ((IBufferWriter<byte>)writer).AsStream();

        Assert.IsFalse(stream.CanRead);
        Assert.IsFalse(stream.CanSeek);
        Assert.IsTrue(stream.CanWrite);

        _ = Assert.ThrowsException<NotSupportedException>(() => stream.Length);
        _ = Assert.ThrowsException<NotSupportedException>(() => stream.Position);

        // Dispose the stream and check that no operation is now allowed
        stream.Dispose();

        Assert.IsFalse(stream.CanRead);
        Assert.IsFalse(stream.CanSeek);
        Assert.IsFalse(stream.CanWrite);
        _ = Assert.ThrowsException<NotSupportedException>(() => stream.Length);
        _ = Assert.ThrowsException<NotSupportedException>(() => stream.Position);
    }

    [TestCategory("IBufferWriterStream")]
    [TestMethod]
    public void Test_IBufferWriterStream_Write_Array()
    {
        ArrayPoolBufferWriter<byte> writer = new();
        Stream stream = ((IBufferWriter<byte>)writer).AsStream();

        byte[] data = Test_MemoryStream.CreateRandomData(64);

        // Write random data to the stream wrapping the buffer writer, and validate
        // that the state of the writer is consistent, and the written content matches.
        stream.Write(data, 0, data.Length);

        Assert.AreEqual(writer.WrittenCount, data.Length);
        Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data));

        // A few tests with invalid inputs (null buffers, invalid indices, etc.)
        _ = Assert.ThrowsException<ArgumentNullException>(() => stream.Write(null!, 0, 10));
        _ = Assert.ThrowsException<ArgumentOutOfRangeException>(() => stream.Write(data, -1, 10));
        _ = Assert.ThrowsException<ArgumentException>(() => stream.Write(data, 200, 10));
        _ = Assert.ThrowsException<ArgumentOutOfRangeException>(() => stream.Write(data, 0, -24));
        _ = Assert.ThrowsException<ArgumentException>(() => stream.Write(data, 0, 200));

        stream.Dispose();

        _ = Assert.ThrowsException<ObjectDisposedException>(() => stream.Write(data, 0, data.Length));
    }

    [TestCategory("IBufferWriterStream")]
    [TestMethod]
    public async Task Test_IBufferWriterStream_WriteAsync_Array()
    {
        ArrayPoolBufferWriter<byte> writer = new();
        Stream stream = ((IBufferWriter<byte>)writer).AsStream();

        byte[] data = Test_MemoryStream.CreateRandomData(64);

        // Same test as above, but using an asynchronous write instead
        await stream.WriteAsync(data, 0, data.Length);

        Assert.AreEqual(writer.WrittenCount, data.Length);
        Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data));

        _ = await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => stream.WriteAsync(null!, 0, 10));
        _ = await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(() => stream.WriteAsync(data, -1, 10));
        _ = await Assert.ThrowsExceptionAsync<ArgumentException>(() => stream.WriteAsync(data, 200, 10));
        _ = await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(() => stream.WriteAsync(data, 0, -24));
        _ = await Assert.ThrowsExceptionAsync<ArgumentException>(() => stream.WriteAsync(data, 0, 200));

        stream.Dispose();

        _ = await Assert.ThrowsExceptionAsync<ObjectDisposedException>(() => stream.WriteAsync(data, 0, data.Length));
    }

    [TestCategory("IBufferWriterStream")]
    [TestMethod]
    public void Test_IBufferWriterStream_WriteByte()
    {
        ArrayPoolBufferWriter<byte> writer = new();
        Stream stream = ((IBufferWriter<byte>)writer).AsStream();

        ReadOnlySpan<byte> data = stackalloc byte[] { 1, 128, 255, 32 };

        foreach (HighPerformance.Enumerables.ReadOnlySpanEnumerable<byte>.Item item in data.Enumerate())
        {
            // Since we're enumerating, we can also double check the current written count
            // at each iteration, to ensure the writes are done correctly every time.
            Assert.AreEqual(writer.WrittenCount, item.Index);

            // Write a number of bytes one by one to test this API as well
            stream.WriteByte(item.Value);
        }

        // Validate the final written length and actual data
        Assert.AreEqual(writer.WrittenCount, data.Length);
        Assert.IsTrue(data.SequenceEqual(writer.WrittenSpan));

        _ = Assert.ThrowsException<NotSupportedException>(() => stream.ReadByte());
    }

    [TestCategory("IBufferWriterStream")]
    [TestMethod]
    public void Test_IBufferWriterStream_Write_Span()
    {
        ArrayPoolBufferWriter<byte> writer = new();
        Stream stream = ((IBufferWriter<byte>)writer).AsStream();

        Memory<byte> data = Test_MemoryStream.CreateRandomData(64);

        // This will use the extension when on .NET Standard 2.0,
        // as the Stream class doesn't have Spam<T> or Memory<T>
        // public APIs there. This is the case eg. on UWP as well.
        stream.Write(data.Span);

        Assert.AreEqual(writer.WrittenCount, data.Length);
        Assert.IsTrue(data.Span.SequenceEqual(writer.WrittenSpan));
    }

    [TestCategory("IBufferWriterStream")]
    [TestMethod]
    public async Task Test_IBufferWriterStream_WriteAsync_Memory()
    {
        ArrayPoolBufferWriter<byte> writer = new();
        Stream stream = ((IBufferWriter<byte>)writer).AsStream();

        Memory<byte> data = Test_MemoryStream.CreateRandomData(64);

        // Same as the other asynchronous test above, but writing from a Memory<T>
        await stream.WriteAsync(data);

        Assert.AreEqual(writer.WrittenCount, data.Length);
        Assert.IsTrue(data.Span.SequenceEqual(writer.WrittenSpan));
    }
}