// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;

namespace Microsoft.AspNetCore.Components.RenderTree;

public class ArrayBuilderTest
{
    private readonly TestArrayPool<int> ArrayPool = new TestArrayPool<int>();

    [Fact]
    public void Append_SingleItem()
    {
        // Arrange
        var value = 7;
        using var builder = CreateArrayBuilder();

        // Act
        builder.Append(value);

        // Assert
        Assert.Equal(1, builder.Count);
        Assert.Equal(value, builder.Buffer[0]);
    }

    [Fact]
    public void Append_ThreeItem()
    {
        // Arrange
        var value1 = 7;
        var value2 = 22;
        var value3 = 3;
        using var builder = CreateArrayBuilder();

        // Act
        builder.Append(value1);
        builder.Append(value2);
        builder.Append(value3);

        // Assert
        Assert.Equal(3, builder.Count);
        Assert.Equal(new[] { value1, value2, value3 }, builder.Buffer.Take(3));
    }

    [Fact]
    public void Append_FillBuffer()
    {
        // Arrange
        var capacity = 8;
        using var builder = new ArrayBuilder<int>(minCapacity: capacity);

        // Act
        for (var i = 0; i < capacity; i++)
        {
            builder.Append(5);
        }

        // Assert
        Assert.Equal(capacity, builder.Count);
        Assert.Equal(Enumerable.Repeat(5, capacity), builder.Buffer.Take(capacity));
    }

    [Fact]
    public void AppendArray_CopySubset()
    {
        // Arrange
        var array = Enumerable.Repeat(8, 5).ToArray();
        using var builder = CreateArrayBuilder();

        // Act
        builder.Append(array, 0, 2);

        // Assert
        Assert.Equal(2, builder.Count);
        Assert.Equal(new[] { 8, 8 }, builder.Buffer.Take(2));
    }

    [Fact]
    public void AppendArray_CopyArray()
    {
        // Arrange
        var array = Enumerable.Repeat(8, 5).ToArray();
        using var builder = CreateArrayBuilder();

        // Act
        builder.Append(array, 0, array.Length);

        // Assert
        Assert.Equal(array.Length, builder.Count);
        Assert.Equal(array, builder.Buffer.Take(array.Length));
    }

    [Fact]
    public void AppendArray_AfterPriorInsertion()
    {
        // Arrange
        var array = Enumerable.Repeat(8, 5).ToArray();
        using var builder = CreateArrayBuilder();

        // Act
        builder.Append(118);
        builder.Append(array, 0, 2);

        // Assert
        Assert.Equal(3, builder.Count);
        Assert.Equal(new[] { 118, 8, 8 }, builder.Buffer.Take(3));
    }

    [Theory]
    // These are at boundaries of our capacity increments.
    [InlineData(1023)]
    [InlineData(1024)]
    [InlineData(1025)]
    public void AppendArray_LargerThanBuffer(int size)
    {
        // Arrange
        var array = Enumerable.Repeat(17, size).ToArray();
        using var builder = CreateArrayBuilder();

        // Act
        builder.Append(array, 0, array.Length);

        // Assert
        Assert.Equal(array.Length, builder.Count);
        Assert.Equal(array, builder.Buffer.Take(array.Length));
    }

    [Fact]
    public void Overwrite_Works()
    {
        // Arrange
        using var builder = CreateArrayBuilder();
        builder.Append(7);
        builder.Append(3);
        builder.Append(9);

        // Act
        builder.Overwrite(1, 2);

        // Assert
        Assert.Equal(3, builder.Count);
        Assert.Equal(new[] { 7, 2, 9 }, builder.Buffer.Take(3));
    }

    [Fact]
    public void Insert_Works()
    {
        // Arrange
        using var builder = CreateArrayBuilder();
        builder.Append(7);
        builder.Append(3);
        builder.Append(9);

        // Act
        builder.InsertExpensive(1, 2);

        // Assert
        Assert.Equal(4, builder.Count);
        Assert.Equal(new[] { 7, 2, 3, 9 }, builder.Buffer.Take(4));
    }

    [Fact]
    public void Insert_WhenBufferIsAtCapacity()
    {
        // Arrange
        using var builder = CreateArrayBuilder(2);
        builder.Append(new[] { 1, 3 }, 0, 2);

        // Act
        builder.InsertExpensive(1, 2);

        // Assert
        Assert.Equal(3, builder.Count);
        Assert.Equal(new[] { 1, 2, 3 }, builder.Buffer.Take(3));
    }

    [Fact]
    public void RemoveLast_Works()
    {
        // Arrange
        using var builder = CreateArrayBuilder();
        builder.Append(1);
        builder.Append(2);
        builder.Append(3);

        // Act
        builder.RemoveLast();

        // Assert
        Assert.Equal(2, builder.Count);
        Assert.Equal(new[] { 1, 2, }, builder.Buffer.Take(2));
    }

    [Fact]
    public void RemoveLast_LastEntry()
    {
        // Arrange
        int[] buffer;
        using (var builder = CreateArrayBuilder())
        {
            builder.Append(1);
            buffer = builder.Buffer;

            // Act
            builder.RemoveLast();

            // Assert
            Assert.Equal(0, builder.Count);
        }

        // Also verify that the buffer is indeed returned in this case.
        var returnedBuffer = Assert.Single(ArrayPool.ReturnedBuffers);
        Assert.Same(buffer, returnedBuffer);
    }

    [Fact]
    public void Clear_ReturnsBuffer()
    {
        // Arrange
        using var builder = CreateArrayBuilder();
        builder.Append(1);
        var buffer = builder.Buffer;

        // Act
        builder.Clear();

        // Assert
        Assert.Equal(0, builder.Count);
        var returnedBuffer = Assert.Single(ArrayPool.ReturnedBuffers);
        Assert.Same(buffer, returnedBuffer);
    }

    [Fact]
    public void Dispose_WithEmptyBuffer_DoesNotReturnIt()
    {
        // Arrange
        var builder = CreateArrayBuilder();

        // Act
        builder.Dispose();

        // Assert
        Assert.Empty(ArrayPool.ReturnedBuffers);
    }

    [Fact]
    public void Dispose_NonEmptyBufferIsReturned()
    {
        // Arrange
        var builder = CreateArrayBuilder();
        builder.Append(1);
        var buffer = builder.Buffer;

        // Act
        builder.Dispose();

        // Assert
        Assert.Single(ArrayPool.ReturnedBuffers);
        var returnedBuffer = Assert.Single(ArrayPool.ReturnedBuffers);
        Assert.Same(buffer, returnedBuffer);
        Assert.NotSame(builder.Buffer, buffer); // Prevents use after free
    }

    [Fact]
    public void DoubleDispose_DoesNotReturnBufferTwice()
    {
        // Arrange
        var builder = CreateArrayBuilder();
        builder.Append(1);
        var buffer = builder.Buffer;

        // Act
        builder.Dispose();
        builder.Dispose();

        // Assert
        Assert.Single(ArrayPool.ReturnedBuffers);
        var returnedBuffer = Assert.Single(ArrayPool.ReturnedBuffers);
        Assert.Same(buffer, returnedBuffer);
    }

    [Fact]
    public void Dispose_ThrowsOnReuse()
    {
        // Arrange
        var builder = CreateArrayBuilder();
        builder.Append(1);
        var buffer = builder.Buffer;

        builder.Dispose();
        Assert.Single(ArrayPool.ReturnedBuffers);

        // Act & Assert
        Assert.Throws<ObjectDisposedException>(() => builder.Append(1));
    }

    [Fact]
    public void UnusedBufferIsReturned_OnResize()
    {
        // Arrange
        var builder = CreateArrayBuilder(2);

        // Act
        for (var i = 0; i < 10; i++)
        {
            builder.Append(i);
        }

        // Assert
        Assert.Collection(
            ArrayPool.ReturnedBuffers,
            buffer => Assert.Equal(2, buffer.Length),
            buffer => Assert.Equal(4, buffer.Length),
            buffer => Assert.Equal(8, buffer.Length));

        // Clear this because this is no longer interesting.
        ArrayPool.ReturnedBuffers.Clear();

        var buffer = builder.Buffer;
        builder.Dispose();

        Assert.Same(buffer, Assert.Single(ArrayPool.ReturnedBuffers));
    }

    private ArrayBuilder<int> CreateArrayBuilder(int capacity = 32)
    {
        return new ArrayBuilder<int>(capacity, ArrayPool);
    }
}
