
namespace QuanTAlib.Tests;

public class RingBufferTests
{
    [Fact]
    public void Constructor_ValidCapacity_CreatesBuffer()
    {
        var buffer = new RingBuffer(10);

        Assert.Equal(10, buffer.Capacity);
        Assert.Equal(0, buffer.Count);
        Assert.False(buffer.IsFull);
        Assert.Equal(0, buffer.Sum);
        Assert.Equal(0, buffer.Average);
    }

    [Fact]
    public void Constructor_ZeroCapacity_ThrowsArgumentException()
    {
        Assert.Throws<ArgumentException>(() => new RingBuffer(0));
    }

    [Fact]
    public void Constructor_NegativeCapacity_ThrowsArgumentException()
    {
        Assert.Throws<ArgumentException>(() => new RingBuffer(-1));
    }

    [Fact]
    public void Add_SingleValue_UpdatesState()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);

        Assert.Equal(1, buffer.Count);
        Assert.Equal(10.0, buffer.Sum);
        Assert.Equal(10.0, buffer.Average);
        Assert.Equal(10.0, buffer.Newest);
        Assert.Equal(10.0, buffer.Oldest);
        Assert.False(buffer.IsFull);
    }

    [Fact]
    public void Add_MultipleValues_UpdatesState()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        Assert.Equal(3, buffer.Count);
        Assert.Equal(60.0, buffer.Sum);
        Assert.Equal(20.0, buffer.Average);
        Assert.Equal(30.0, buffer.Newest);
        Assert.Equal(10.0, buffer.Oldest);
        Assert.False(buffer.IsFull);
    }

    [Fact]
    public void Add_FillBuffer_BecomesFullAndWraps()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        Assert.Equal(3, buffer.Count);
        Assert.True(buffer.IsFull);
        Assert.Equal(60.0, buffer.Sum);
        Assert.Equal(20.0, buffer.Average);

        // Add one more - should remove 10.0
        double removed = buffer.Add(40.0);

        Assert.Equal(10.0, removed);
        Assert.Equal(3, buffer.Count);
        Assert.True(buffer.IsFull);
        Assert.Equal(90.0, buffer.Sum); // 20 + 30 + 40
        Assert.Equal(30.0, buffer.Average);
        Assert.Equal(40.0, buffer.Newest);
        Assert.Equal(20.0, buffer.Oldest);
    }

    [Fact]
    public void Add_MultipleWraps_MaintainsCorrectState()
    {
        var buffer = new RingBuffer(3);

        // Fill and wrap multiple times
        for (int i = 1; i <= 10; i++)
        {
            buffer.Add(i * 10.0);
        }

        // Should contain: 80, 90, 100
        Assert.Equal(3, buffer.Count);
        Assert.True(buffer.IsFull);
        Assert.Equal(270.0, buffer.Sum); // 80 + 90 + 100
        Assert.Equal(90.0, buffer.Average);
        Assert.Equal(100.0, buffer.Newest);
        Assert.Equal(80.0, buffer.Oldest);
    }

    [Fact]
    public void UpdateNewest_ModifiesLastValue()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        Assert.Equal(60.0, buffer.Sum);

        buffer.UpdateNewest(35.0);

        Assert.Equal(65.0, buffer.Sum); // 10 + 20 + 35
        Assert.Equal(35.0, buffer.Newest);
        Assert.Equal(3, buffer.Count);
    }

    [Fact]
    public void UpdateNewest_EmptyBuffer_DoesNothing()
    {
        var buffer = new RingBuffer(5);

        buffer.UpdateNewest(100.0); // Should not throw

        Assert.Equal(0, buffer.Count);
        Assert.Equal(0, buffer.Sum);
    }

    [Fact]
    public void Indexer_AccessesCorrectValues()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        // Index 0 = oldest, Index 2 = newest
        Assert.Equal(10.0, buffer[0]);
        Assert.Equal(20.0, buffer[1]);
        Assert.Equal(30.0, buffer[2]);
    }

    [Fact]
    public void Indexer_AfterWrap_AccessesCorrectValues()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);
        buffer.Add(40.0); // Wraps, removes 10

        // Should contain: 20, 30, 40
        Assert.Equal(20.0, buffer[0]);
        Assert.Equal(30.0, buffer[1]);
        Assert.Equal(40.0, buffer[2]);
    }

    [Fact]
    public void Indexer_OutOfRange_ThrowsException()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);

        // Valid indices are 0 and 1 (2 elements)
        // Index 2 should throw ArgumentOutOfRangeException
        Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[2]);
        Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[10]);
    }

    [Fact]
    public void Clear_ResetsState()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        buffer.Clear();

        Assert.Equal(0, buffer.Count);
        Assert.Equal(0, buffer.Sum);
        Assert.Equal(0, buffer.Average);
        Assert.False(buffer.IsFull);
    }

    [Fact]
    public void Clear_AllowsReuse()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);
        buffer.Clear();

        buffer.Add(100.0);

        Assert.Equal(1, buffer.Count);
        Assert.Equal(100.0, buffer.Sum);
        Assert.Equal(100.0, buffer.Newest);
    }

    [Fact]
    public void Clone_CreatesIndependentCopy()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        var clone = buffer.Clone();

        // Verify clone has same state
        Assert.Equal(buffer.Count, clone.Count);
        Assert.Equal(buffer.Sum, clone.Sum);
        Assert.Equal(buffer.Newest, clone.Newest);
        Assert.Equal(buffer.Oldest, clone.Oldest);

        // Modify original - clone should be unaffected
        buffer.Add(40.0);

        Assert.Equal(4, buffer.Count);
        Assert.Equal(3, clone.Count);
        Assert.Equal(100.0, buffer.Sum);
        Assert.Equal(60.0, clone.Sum);
    }

    [Fact]
    public void CopyFrom_CopiesState()
    {
        var source = new RingBuffer(5);
        var target = new RingBuffer(5);

        source.Add(10.0);
        source.Add(20.0);
        source.Add(30.0);

        target.Add(100.0); // Different initial state

        target.CopyFrom(source);

        Assert.Equal(source.Count, target.Count);
        Assert.Equal(source.Sum, target.Sum);
        Assert.Equal(source.Newest, target.Newest);
        Assert.Equal(source.Oldest, target.Oldest);

        // Verify independence after copy
        source.Add(40.0);
        Assert.NotEqual(source.Sum, target.Sum);
    }

    [Fact]
    public void CopyFrom_DifferentCapacity_ThrowsException()
    {
        var source = new RingBuffer(5);
        var target = new RingBuffer(10);

        Assert.Throws<ArgumentException>(() => target.CopyFrom(source));
    }

    [Fact]
    public void GetSpan_ReturnsChronologicalOrder()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        var span = buffer.GetSpan();

        Assert.Equal(3, span.Length);
        Assert.Equal(10.0, span[0]);
        Assert.Equal(20.0, span[1]);
        Assert.Equal(30.0, span[2]);
    }

    [Fact]
    public void GetSpan_AfterWrap_ReturnsChronologicalOrder()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);
        buffer.Add(40.0);
        buffer.Add(50.0);

        var span = buffer.GetSpan();

        // Should be: 30, 40, 50
        Assert.Equal(3, span.Length);
        Assert.Equal(30.0, span[0]);
        Assert.Equal(40.0, span[1]);
        Assert.Equal(50.0, span[2]);
    }

    [Fact]
    public void GetSpan_EmptyBuffer_ReturnsEmpty()
    {
        var buffer = new RingBuffer(5);

        var span = buffer.GetSpan();

        Assert.True(span.IsEmpty);
    }

    [Fact]
    public void Min_ReturnsMinimumValue()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(30.0);
        buffer.Add(10.0);
        buffer.Add(50.0);
        buffer.Add(20.0);
        buffer.Add(40.0);

        Assert.Equal(10.0, buffer.Min());
    }

    [Fact]
    public void Max_ReturnsMaximumValue()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(30.0);
        buffer.Add(10.0);
        buffer.Add(50.0);
        buffer.Add(20.0);
        buffer.Add(40.0);

        Assert.Equal(50.0, buffer.Max());
    }

    [Fact]
    public void Min_EmptyBuffer_ReturnsNaN()
    {
        var buffer = new RingBuffer(5);

        Assert.True(double.IsNaN(buffer.Min()));
    }

    [Fact]
    public void Max_EmptyBuffer_ReturnsNaN()
    {
        var buffer = new RingBuffer(5);

        Assert.True(double.IsNaN(buffer.Max()));
    }

    [Fact]
    public void Enumerator_IteratesInChronologicalOrder()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        List<double> values = [];
        foreach (var v in buffer)
        {
            values.Add(v);
        }

        Assert.Equal(3, values.Count);
        Assert.Equal(10.0, values[0]);
        Assert.Equal(20.0, values[1]);
        Assert.Equal(30.0, values[2]);
    }

    [Fact]
    public void Enumerator_AfterWrap_IteratesInChronologicalOrder()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);
        buffer.Add(40.0);
        buffer.Add(50.0);

        List<double> values = [];
        foreach (var v in buffer)
        {
            values.Add(v);
        }

        // Should be: 30, 40, 50
        Assert.Equal(3, values.Count);
        Assert.Equal(30.0, values[0]);
        Assert.Equal(40.0, values[1]);
        Assert.Equal(50.0, values[2]);
    }

    [Fact]
    public void Add_WithIsNew_WorksCorrectly()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0, isNew: true);
        buffer.Add(20.0, isNew: true);
        buffer.Add(25.0, isNew: false); // Should update 20.0 to 25.0

        Assert.Equal(2, buffer.Count);
        Assert.Equal(25.0, buffer.Newest);
        Assert.Equal(35.0, buffer.Sum); // 10 + 25
    }

    [Fact]
    public void Indexer_WithIndexType_SupportsFromEnd()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        Assert.Equal(30.0, buffer[^1]); // Newest
        Assert.Equal(20.0, buffer[^2]);
        Assert.Equal(10.0, buffer[^3]); // Oldest
    }

    [Fact]
    public void Newest_EmptyBuffer_ReturnsNaN()
    {
        var buffer = new RingBuffer(5);

        Assert.True(double.IsNaN(buffer.Newest));
    }

    [Fact]
    public void Oldest_EmptyBuffer_ReturnsNaN()
    {
        var buffer = new RingBuffer(5);

        Assert.True(double.IsNaN(buffer.Oldest));
    }

    [Fact]
    public void Average_EmptyBuffer_ReturnsZero()
    {
        var buffer = new RingBuffer(5);

        Assert.Equal(0, buffer.Average);
    }

    [Fact]
    public void GetInternalSpan_ReturnsFullBuffer()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);

        var span = buffer.GetInternalSpan();

        Assert.Equal(5, span.Length); // Full capacity, not count
    }

    [Fact]
    public void ToArray_AfterWrap_ReturnsChronologicalOrder()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);
        buffer.Add(40.0); // Wraps

        var arr = buffer.ToArray();

        Assert.Equal(3, arr.Length);
        Assert.Equal(20.0, arr[0]);
        Assert.Equal(30.0, arr[1]);
        Assert.Equal(40.0, arr[2]);
    }

    [Fact]
    public void CopyTo_AfterWrap_CopiesInChronologicalOrder()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);
        buffer.Add(40.0); // Wraps

        var dest = new double[5];
        buffer.CopyTo(dest, 1);

        Assert.Equal(0, dest[0]); // Untouched
        Assert.Equal(20.0, dest[1]);
        Assert.Equal(30.0, dest[2]);
        Assert.Equal(40.0, dest[3]);
        Assert.Equal(0, dest[4]); // Untouched
    }

    [Fact]
    public void Indexer_Set_UpdatesValueAndSum()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        Assert.Equal(60.0, buffer.Sum);

        buffer[1] = 25.0; // Change 20.0 to 25.0

        Assert.Equal(65.0, buffer.Sum);
        Assert.Equal(25.0, buffer[1]);
    }

    [Fact]
    public void Indexer_SetFromEnd_UpdatesValueAndSum()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        buffer[^1] = 35.0; // Change newest (30.0) to 35.0

        Assert.Equal(65.0, buffer.Sum);
        Assert.Equal(35.0, buffer[^1]);
    }

    [Fact]
    public void Min_LargeBuffer_UsesSimd()
    {
        var buffer = new RingBuffer(100);

        for (int i = 0; i < 100; i++)
        {
            buffer.Add(i + 1); // 1 to 100
        }

        Assert.Equal(1.0, buffer.Min());
    }

    [Fact]
    public void Max_LargeBuffer_UsesSimd()
    {
        var buffer = new RingBuffer(100);

        for (int i = 0; i < 100; i++)
        {
            buffer.Add(i + 1); // 1 to 100
        }

        Assert.Equal(100.0, buffer.Max());
    }

    [Fact]
    public void ToArray_EmptyBuffer_ReturnsEmpty()
    {
        var buffer = new RingBuffer(5);

        var arr = buffer.ToArray();

        Assert.Empty(arr);
    }

    [Fact]
    public void CopyTo_EmptyBuffer_DoesNothing()
    {
        var buffer = new RingBuffer(5);
        double[] dest = [1.0, 2.0, 3.0];

        buffer.CopyTo(dest, 0);

        Assert.Equal(1.0, dest[0]);
        Assert.Equal(2.0, dest[1]);
        Assert.Equal(3.0, dest[2]);
    }

    [Fact]
    public void InternalBuffer_ReturnsSpan()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);

        var span = buffer.InternalBuffer;

        Assert.Equal(5, span.Length);
        Assert.Equal(10.0, span[0]);
    }

    [Fact]
    public void Enumerator_Reset_AllowsReIteration()
    {
        var buffer = new RingBuffer(3);
        buffer.Add(10.0);
        buffer.Add(20.0);

        var enumerator = buffer.GetEnumerator();

        // First iteration
        Assert.True(enumerator.MoveNext());
        Assert.Equal(10.0, enumerator.Current);
        Assert.True(enumerator.MoveNext());
        Assert.Equal(20.0, enumerator.Current);
        Assert.False(enumerator.MoveNext());

        // Reset and iterate again
        enumerator.Reset();
        Assert.True(enumerator.MoveNext());
        Assert.Equal(10.0, enumerator.Current);

        enumerator.Dispose(); // Coverage for Dispose
    }

    [Fact]
    public void IEnumerable_GetEnumerator_Works()
    {
        var buffer = new RingBuffer(3);
        buffer.Add(10.0);
        buffer.Add(20.0);

        IEnumerable<double> enumerable = buffer;
        List<double> values = [];
        foreach (var v in enumerable)
        {
            values.Add(v);
        }

        Assert.Equal(2, values.Count);
        Assert.Equal(10.0, values[0]);
        Assert.Equal(20.0, values[1]);
    }

    [Fact]
    public void IEnumerable_NonGeneric_GetEnumerator_Works()
    {
        var buffer = new RingBuffer(3);
        buffer.Add(10.0);
        buffer.Add(20.0);

        IEnumerable enumerable = buffer;
        List<double> values = [];
        foreach (var v in enumerable)
        {
            values.Add((double)v);
        }

        Assert.Equal(2, values.Count);
    }

    [Fact]
    public void Indexer_Set_AfterWrap_UpdatesCorrectly()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);
        buffer.Add(40.0); // Wraps - now has 20, 30, 40

        buffer[0] = 25.0; // Change oldest (20.0) to 25.0

        Assert.Equal(95.0, buffer.Sum); // 25 + 30 + 40
        Assert.Equal(25.0, buffer[0]);
    }

    [Fact]
    public void Add_WithIsNew_EmptyBuffer_AddsValue()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0, isNew: false); // isNew=false but buffer empty, should still add

        Assert.Equal(1, buffer.Count);
        Assert.Equal(10.0, buffer.Newest);
    }

    [Fact]
    public void BarCorrection_Workflow()
    {
        // Simulate bar correction (isNew=false) workflow
        var buffer = new RingBuffer(3);
        var backup = new RingBuffer(3);

        // Add values as new bars
        buffer.Add(10.0);
        backup.CopyFrom(buffer);

        buffer.Add(20.0);
        backup.CopyFrom(buffer);

        buffer.Add(30.0);
        backup.CopyFrom(buffer);

        double avgBeforeCorrection = buffer.Average;

        // Simulate correction (isNew=false)
        buffer.CopyFrom(backup); // Restore previous state
        buffer.UpdateNewest(35.0); // Update with corrected value

        // Average should reflect the correction
        Assert.Equal(21.666666666666668, buffer.Average, 1e-10);
        Assert.NotEqual(avgBeforeCorrection, buffer.Average);
    }

    [Fact]
    public void Snapshot_CapturesCurrentState()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        buffer.Snapshot();

        // Modify buffer after snapshot
        buffer.Add(40.0);

        Assert.Equal(4, buffer.Count);
        Assert.Equal(100.0, buffer.Sum); // 10 + 20 + 30 + 40
    }

    [Fact]
    public void Restore_ReturnsToSnapshotState()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        buffer.Snapshot();
        double sumBeforeModification = buffer.Sum;
        int countBeforeModification = buffer.Count;

        // Modify buffer after snapshot
        buffer.Add(40.0);
        Assert.Equal(4, buffer.Count);

        // Restore to snapshot state
        buffer.Restore();

        Assert.Equal(countBeforeModification, buffer.Count);
        Assert.Equal(sumBeforeModification, buffer.Sum);
    }

    [Fact]
    public void Snapshot_Restore_WithWrapping()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        buffer.Snapshot();

        // Add value that causes wrap
        buffer.Add(40.0);
        Assert.Equal(90.0, buffer.Sum); // 20 + 30 + 40

        buffer.Restore();

        Assert.Equal(60.0, buffer.Sum); // 10 + 20 + 30
        Assert.Equal(30.0, buffer.Newest);
    }

    [Fact]
    public void RecalculateSum_CorrectsDrift()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        double recalculated = buffer.RecalculateSum();

        Assert.Equal(60.0, recalculated);
        Assert.Equal(60.0, buffer.Sum);
    }

    [Fact]
    public void RecalculateSum_AfterMultipleOperations()
    {
        var buffer = new RingBuffer(3);

        // Simulate many operations that could accumulate floating-point drift
        for (int i = 0; i < 100; i++)
        {
            buffer.Add(i * 0.1);
        }

        double recalculated = buffer.RecalculateSum();

        // Should be equal (or very close) since we're using exact values
        Assert.Equal(recalculated, buffer.Sum);
    }

    [Fact]
    public void StartIndex_EmptyBuffer_ReturnsZero()
    {
        var buffer = new RingBuffer(5);

        Assert.Equal(0, buffer.StartIndex);
    }

    [Fact]
    public void StartIndex_PartiallyFilled_ReturnsZero()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);

        Assert.Equal(0, buffer.StartIndex);
    }

    [Fact]
    public void StartIndex_FullBuffer_ReturnsHead()
    {
        var buffer = new RingBuffer(3);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);
        buffer.Add(40.0); // Wraps

        // StartIndex should point to oldest element
        Assert.True(buffer.StartIndex >= 0 && buffer.StartIndex < buffer.Capacity);
        Assert.Equal(20.0, buffer.Oldest);
    }

    [Fact]
    public void Indexer_NegativeIndexViaFromEnd_ThrowsWhenOutOfBounds()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        // ^4 when count=3 should throw
        Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[^4]);
    }

    [Fact]
    public void CopyTo_InsufficientDestinationBuffer_Behavior()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);
        buffer.Add(30.0);

        var dest = new double[2]; // Too small

        // This will throw IndexOutOfRangeException since we're copying 3 elements to size-2 array
        Assert.Throws<ArgumentException>(() => buffer.CopyTo(dest, 0));
    }

    [Fact]
    public void CopyTo_StartIndexOutOfRange_Behavior()
    {
        var buffer = new RingBuffer(5);

        buffer.Add(10.0);
        buffer.Add(20.0);

        var dest = new double[5];

        // Starting at index 4 with 2 elements should fail
        Assert.Throws<ArgumentException>(() => buffer.CopyTo(dest, 4));
    }
}
