namespace QuanTAlib.Tests; public class TSeriesTests { [Fact] public void Constructor_Default_CreatesEmptySeries() { var series = new TSeries(); Assert.Empty(series); } [Fact] public void Constructor_WithCapacity_CreatesEmptySeries() { var series = new TSeries(100); Assert.Empty(series); } [Fact] public void Constructor_WithLists_WrapsExistingData() { var times = new List { 100, 200, 300 }; var values = new List { 1.0, 2.0, 3.0 }; var series = new TSeries(times, values); Assert.Equal(3, series.Count); Assert.Equal(1.0, series[0].Value); Assert.Equal(2.0, series[1].Value); Assert.Equal(3.0, series[2].Value); } [Fact] public void Name_DefaultValue_IsData() { var series = new TSeries(); Assert.Equal("Data", series.Name); } [Fact] public void Name_CanBeSet() { var series = new TSeries { Name = "TestSeries" }; Assert.Equal("TestSeries", series.Name); } [Fact] public void Add_NewValue_IncreasesCount() { var series = new TSeries(); long time = DateTime.UtcNow.Ticks; series.Add(time, 10.0, isNew: true); Assert.Single(series); Assert.Equal(10.0, series.Last.Value); } [Fact] public void Add_UpdateValue_DoesNotIncreaseCount() { var series = new TSeries(); long time = DateTime.UtcNow.Ticks; series.Add(time, 10.0, isNew: true); series.Add(time, 11.0, isNew: false); Assert.Single(series); Assert.Equal(11.0, series.Last.Value); } [Fact] public void Add_MultipleValues_MaintainsOrder() { var series = new TSeries(); long t0 = DateTime.UtcNow.Ticks; long t1 = t0 + TimeSpan.TicksPerMinute; series.Add(t0, 10.0, isNew: true); series.Add(t1, 20.0, isNew: true); Assert.Equal(2, series.Count); Assert.Equal(10.0, series[0].Value); Assert.Equal(20.0, series[1].Value); } [Fact] public void Add_TValue_AddsNewItem() { var series = new TSeries(); var tv = new TValue(DateTime.UtcNow.Ticks, 42.0); series.Add(tv); Assert.Single(series); Assert.Equal(42.0, series.Last.Value); } [Fact] public void Add_TValueWithIsNew_AddsOrUpdates() { var series = new TSeries(); var tv1 = new TValue(DateTime.UtcNow.Ticks, 42.0); var tv2 = new TValue(DateTime.UtcNow.Ticks, 43.0); series.Add(tv1, isNew: true); series.Add(tv2, isNew: false); Assert.Single(series); Assert.Equal(43.0, series.Last.Value); } [Fact] public void Add_WithDateTime_AddsValue() { var series = new TSeries(); var dt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); series.Add(dt, 100.0, isNew: true); Assert.Single(series); Assert.Equal(dt.Ticks, series.Last.Time); Assert.Equal(100.0, series.Last.Value); } [Fact] public void Add_EnumerableDoubles_AddsAllValues() { var series = new TSeries(); var values = new[] { 1.0, 2.0, 3.0, 4.0, 5.0 }; series.Add(values); Assert.Equal(5, series.Count); Assert.Equal(1.0, series[0].Value); Assert.Equal(5.0, series[4].Value); } [Fact] public void Add_UpdateOnEmptySeries_AddsNewItem() { var series = new TSeries(); var tv = new TValue(DateTime.UtcNow.Ticks, 42.0); series.Add(tv, isNew: false); Assert.Single(series); Assert.Equal(42.0, series.Last.Value); } [Fact] public void Last_EmptySeries_ReturnsDefault() { var series = new TSeries(); var last = series.Last; Assert.Equal(0, last.Time); Assert.Equal(0.0, last.Value); } [Fact] public void Last_NonEmptySeries_ReturnsLastValue() { var series = new TSeries(); series.Add(100, 10.0); series.Add(200, 20.0); var last = series.Last; Assert.Equal(200, last.Time); Assert.Equal(20.0, last.Value); } [Fact] public void LastValue_EmptySeries_ReturnsNaN() { var series = new TSeries(); Assert.True(double.IsNaN(series.LastValue)); } [Fact] public void LastValue_NonEmptySeries_ReturnsLastValue() { var series = new TSeries(); series.Add(100, 10.0); series.Add(200, 20.0); Assert.Equal(20.0, series.LastValue); } [Fact] public void LastTime_EmptySeries_ReturnsZero() { var series = new TSeries(); Assert.Equal(0, series.LastTime); } [Fact] public void LastTime_NonEmptySeries_ReturnsLastTime() { var series = new TSeries(); series.Add(100, 10.0); series.Add(200, 20.0); Assert.Equal(200, series.LastTime); } [Fact] public void Values_ReturnsReadOnlySpanOfValues() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); series.Add(300, 3.0); ReadOnlySpan values = series.Values; Assert.Equal(3, values.Length); Assert.Equal(1.0, values[0]); Assert.Equal(2.0, values[1]); Assert.Equal(3.0, values[2]); } [Fact] public void Times_ReturnsReadOnlySpanOfTimes() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); series.Add(300, 3.0); ReadOnlySpan times = series.Times; Assert.Equal(3, times.Length); Assert.Equal(100, times[0]); Assert.Equal(200, times[1]); Assert.Equal(300, times[2]); } [Fact] public void Indexer_ReturnsCorrectTValue() { var series = new TSeries(); series.Add(100, 10.0); series.Add(200, 20.0); series.Add(300, 30.0); Assert.Equal(100, series[0].Time); Assert.Equal(10.0, series[0].Value); Assert.Equal(200, series[1].Time); Assert.Equal(20.0, series[1].Value); Assert.Equal(300, series[2].Time); Assert.Equal(30.0, series[2].Value); } [Fact] public void GetEnumerator_IteratesAllValues() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); series.Add(300, 3.0); var list = series.ToList(); Assert.Equal(3, list.Count); Assert.Equal(1.0, list[0].Value); Assert.Equal(2.0, list[1].Value); Assert.Equal(3.0, list[2].Value); } [Fact] public void GetEnumerator_NonGeneric_Works() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); var list = new List(); IEnumerable enumerable = series; #pragma warning disable S4158 foreach (var item in enumerable) { list.Add(item); } Assert.Equal(2, series.Count); Assert.Equal(2, list.Count); } [Fact] public void Pub_Event_IsRaisedOnAdd() { var series = new TSeries(); TValue? received = null; series.Pub += (object? sender, in TValueEventArgs args) => received = args.Value; series.Add(100, 42.0); Assert.NotNull(received); Assert.Equal(100, received.Value.Time); Assert.Equal(42.0, received.Value.Value); } [Fact] public void Pub_Event_IsRaisedOnUpdate() { var series = new TSeries(); TValue? received = null; series.Add(100, 42.0); series.Pub += (object? sender, in TValueEventArgs args) => received = args.Value; series.Add(100, 43.0, isNew: false); Assert.NotNull(received); Assert.Equal(43.0, received.Value.Value); } [Fact] public void Count_ReturnsCorrectValue() { var series = new TSeries(); series.Add(100, 1.0); Assert.Single(series); series.Add(200, 2.0); Assert.Equal(2, series.Count); } [Fact] public void Constructor_WithMismatchedLists_WrapsData() { // TSeries wraps the lists directly if they're List, no length validation var times = new List { 100, 200, 300 }; var values = new List { 1.0, 2.0 }; // Different length var series = new TSeries(times, values); // Count is based on values list Assert.Equal(2, series.Count); } [Fact] public void Indexer_OutOfBounds_ThrowsException() { var series = new TSeries(); Assert.Throws(() => _ = series[0]); } [Fact] public void Indexer_NegativeIndex_ThrowsException() { var series = new TSeries(); series.Add(100, 1.0); #pragma warning disable DS003 // Invalid index - intentional for testing exception Assert.Throws(() => _ = series[-1]); #pragma warning restore DS003 } [Fact] public void Indexer_BeyondCount_ThrowsException() { var series = new TSeries(); series.Add(100, 1.0); Assert.Throws(() => _ = series[1]); } [Fact] public void Values_EmptySeries_ReturnsEmptySpan() { var series = new TSeries(); ReadOnlySpan values = series.Values; Assert.Equal(0, values.Length); } [Fact] public void Times_EmptySeries_ReturnsEmptySpan() { var series = new TSeries(); ReadOnlySpan times = series.Times; Assert.Equal(0, times.Length); } [Fact] public void Add_WithNaN_PreservesNaN() { var series = new TSeries(); series.Add(100, double.NaN); Assert.True(double.IsNaN(series.Last.Value)); Assert.True(double.IsNaN(series.LastValue)); } [Fact] public void Add_WithInfinity_PreservesInfinity() { var series = new TSeries(); series.Add(100, double.PositiveInfinity); Assert.True(double.IsPositiveInfinity(series.Last.Value)); Assert.True(double.IsPositiveInfinity(series.LastValue)); } [Fact] public void Add_WithNegativeInfinity_PreservesNegativeInfinity() { var series = new TSeries(); series.Add(100, double.NegativeInfinity); Assert.True(double.IsNegativeInfinity(series.Last.Value)); } [Fact] public void Add_EnumerableDoubles_GeneratesIncreasingTimes() { var series = new TSeries(); var values = new[] { 1.0, 2.0, 3.0 }; series.Add(values); Assert.Equal(3, series.Count); // Times should be increasing by TicksPerMinute Assert.True(series[1].Time > series[0].Time); Assert.True(series[2].Time > series[1].Time); Assert.Equal(TimeSpan.TicksPerMinute, series[1].Time - series[0].Time); } [Fact] public void Add_EnumerableDoubles_EmptyArray_AddsNothing() { var series = new TSeries(); series.Add(Array.Empty()); Assert.Empty(series); } [Fact] public void Pub_EventArgs_ContainsIsNewFlag() { var series = new TSeries(); bool? receivedIsNew = null; series.Pub += (object? sender, in TValueEventArgs args) => receivedIsNew = args.IsNew; series.Add(new TValue(100, 42.0), isNew: true); Assert.True(receivedIsNew); series.Add(new TValue(100, 43.0), isNew: false); Assert.False(receivedIsNew); } [Fact] public void Constructor_WithCapacity_DoesNotAffectCount() { var series = new TSeries(1000); Assert.Empty(series); } [Fact] public void Add_WithDateTimeLocal_ConvertsToUtc() { var series = new TSeries(); var localTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Local); series.Add(localTime, 100.0); // The stored time should be UTC var storedTime = new DateTime(series.Last.Time, DateTimeKind.Utc); Assert.Equal(DateTimeKind.Utc, storedTime.Kind); } [Fact] public void Add_WithDateTimeUnspecified_TreatsAsLocal() { var series = new TSeries(); var unspecifiedTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Unspecified); series.Add(unspecifiedTime, 100.0); Assert.Single(series); } [Fact] public void Values_ModifyingUnderlyingList_ReflectsInSpan() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); // Get the span ReadOnlySpan values1 = series.Values; Assert.Equal(2, values1.Length); // Add more data series.Add(300, 3.0); // Get new span - should reflect the change ReadOnlySpan values2 = series.Values; Assert.Equal(3, values2.Length); Assert.Equal(3.0, values2[2]); } [Fact] public void Constructor_WithReadOnlyLists_CopiesData() { // Using arrays which implement IReadOnlyList but aren't List long[] times = [100, 200, 300]; IReadOnlyList values = [1.0, 2.0, 3.0]; var series = new TSeries(times, values); Assert.Equal(3, series.Count); Assert.Equal(1.0, series[0].Value); Assert.Equal(3.0, series[2].Value); } [Fact] public void GetEnumerator_ExplicitGenericInterface_Works() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); series.Add(300, 3.0); // Explicitly call IEnumerable.GetEnumerator() through interface cast IEnumerable genericEnumerable = series; using var enumerator = genericEnumerable.GetEnumerator(); var values = new List(); while (enumerator.MoveNext()) { values.Add(enumerator.Current.Value); } Assert.Equal(3, values.Count); Assert.Equal(1.0, values[0]); Assert.Equal(2.0, values[1]); Assert.Equal(3.0, values[2]); } [Fact] public void GetEnumerator_ExplicitNonGenericInterface_Works() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); // Explicitly call IEnumerable.GetEnumerator() through interface cast IEnumerable nonGenericEnumerable = series; var enumerator = nonGenericEnumerable.GetEnumerator(); var values = new List(); while (enumerator.MoveNext()) { var tv = (TValue)enumerator.Current; values.Add(tv.Value); } Assert.Equal(2, values.Count); Assert.Equal(1.0, values[0]); Assert.Equal(2.0, values[1]); } // ──────────────────────────────────────────────────────────────────── // NEW TESTS: ShareStorageTag struct // ──────────────────────────────────────────────────────────────────── [Fact] public void ShareStorageTag_Instance_IsDefault() { var tag = ShareStorageTag.Instance; // It's a readonly struct with no fields — default should work Assert.Equal(default(ShareStorageTag), tag); } [Fact] public void ShareStorageTag_TwoInstances_AreEqual() { var a = ShareStorageTag.Instance; var b = ShareStorageTag.Instance; Assert.Equal(a, b); } // ──────────────────────────────────────────────────────────────────── // NEW TESTS: TSeriesEnumerator struct // ──────────────────────────────────────────────────────────────────── [Fact] public void TSeriesEnumerator_Reset_AllowsReIteration() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); var enumerator = series.GetEnumerator(); // First pass int count1 = 0; while (enumerator.MoveNext()) { count1++; } Assert.Equal(2, count1); // Reset and re-iterate enumerator.Reset(); int count2 = 0; while (enumerator.MoveNext()) { count2++; } Assert.Equal(2, count2); } [Fact] public void TSeriesEnumerator_Dispose_DoesNotThrow() { var series = new TSeries(); series.Add(100, 1.0); var enumerator = series.GetEnumerator(); enumerator.MoveNext(); enumerator.Dispose(); // Should be no-op Assert.Equal(1.0, enumerator.Current.Value); } [Fact] public void TSeriesEnumerator_Equals_SameState_ReturnsTrue() { var series = new TSeries(); series.Add(100, 1.0); var a = series.GetEnumerator(); var b = series.GetEnumerator(); // Both at initial state (_index = -1) Assert.True(a.Equals(b)); Assert.True(a == b); Assert.False(a != b); } [Fact] public void TSeriesEnumerator_Equals_DifferentState_ReturnsFalse() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); var a = series.GetEnumerator(); var b = series.GetEnumerator(); a.MoveNext(); // a is at index 0, b is at -1 Assert.False(a.Equals(b)); Assert.False(a == b); Assert.True(a != b); } [Fact] public void TSeriesEnumerator_Equals_Object_SameState_ReturnsTrue() { var series = new TSeries(); series.Add(100, 1.0); var a = series.GetEnumerator(); object b = series.GetEnumerator(); Assert.True(a.Equals(b)); } [Fact] public void TSeriesEnumerator_Equals_Object_DifferentType_ReturnsFalse() { var series = new TSeries(); series.Add(100, 1.0); var a = series.GetEnumerator(); Assert.False(a.Equals("not an enumerator")); Assert.False(a.Equals(null)); } [Fact] public void TSeriesEnumerator_GetHashCode_SameState_SameHash() { var series = new TSeries(); series.Add(100, 1.0); var a = series.GetEnumerator(); var b = series.GetEnumerator(); Assert.Equal(a.GetHashCode(), b.GetHashCode()); } [Fact] public void TSeriesEnumerator_GetHashCode_DifferentState_LikelyDifferentHash() { var series = new TSeries(); series.Add(100, 1.0); series.Add(200, 2.0); var a = series.GetEnumerator(); var b = series.GetEnumerator(); a.MoveNext(); Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); } [Fact] public void TSeriesEnumerator_Current_ViaIEnumerator_ReturnsBoxedTValue() { var series = new TSeries(); series.Add(100, 42.0); IEnumerator enumerator = series.GetEnumerator(); enumerator.MoveNext(); object current = enumerator.Current; Assert.IsType(current); Assert.Equal(42.0, ((TValue)current).Value); } [Fact] public void TSeriesEnumerator_DifferentSeries_NotEqual() { var series1 = new TSeries(); series1.Add(100, 1.0); var series2 = new TSeries(); series2.Add(100, 1.0); var a = series1.GetEnumerator(); var b = series2.GetEnumerator(); // Different underlying list references -> not equal Assert.False(a.Equals(b)); } // ──────────────────────────────────────────────────────────────────── // NEW TESTS: Constructor defensive copy verification // ──────────────────────────────────────────────────────────────────── [Fact] public void Constructor_WithReadOnlyLists_MakesDefensiveCopy() { var times = new List { 100, 200 }; var values = new List { 1.0, 2.0 }; // Use IReadOnlyList constructor which makes defensive copies var series = new TSeries((IReadOnlyList)times, (IReadOnlyList)values); // Mutate original lists times.Add(300); values.Add(3.0); // Series should NOT reflect the mutation (defensive copy was made) Assert.Equal(2, series.Count); } [Fact] public void Constructor_WithReadOnlyLists_OriginalMutationDoesNotAffectSeries() { long[] originalTimes = [100, 200, 300]; double[] originalValues = [1.0, 2.0, 3.0]; var series = new TSeries(originalTimes, originalValues); // Mutate original arrays originalValues[0] = 999.0; // Series should NOT reflect the mutation (defensive copy was made) Assert.Equal(1.0, series[0].Value); } }