using Microsoft.VisualStudio.TestTools.UnitTesting; using MarketData.Cache; using MarketData.MarketDataModel; using System; using System.Collections.Generic; using MarketData.Utils; namespace MarketDataUnitTests; [TestClass] public class LocalPriceCacheTests { private LocalPriceCache cache = default!; [TestInitialize] public void Setup() { cache = LocalPriceCache.GetInstance(); cache.Clear(); } [TestCleanup] public void Cleanup() { if (cache != null) cache.Dispose(); } private Price CreatePrice(string symbol, DateTime date, double close) { return new Price { Symbol = symbol, Date = date, Close = close, Open = close - 1, High = close + 1, Low = close - 2, AdjClose = close, Volume = 1000, Source = Price.PriceSource.Other }; } #region Basic Add/Get Tests [TestMethod] public void AddAndGetPrice_SinglePrice_ShouldReturnPrice() { string symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); Price price = CreatePrice(symbol, date, 250); cache.Add(price); Price cachedPrice = cache.GetPrice(symbol, date); Assert.IsNotNull(cachedPrice); Assert.AreEqual(250, cachedPrice.Close); } [TestMethod] public void AddAndGetPrices_List_ShouldReturnAllPrices() { DateTime date = new DateTime(2026, 2, 22); List symbols = new List { "MSFT", "AAPL", "GOOG" }; Prices prices = new Prices(); foreach (string symbol in symbols) { prices.Add(CreatePrice(symbol, date, 100 + symbols.IndexOf(symbol) * 10)); } cache.Add(prices); foreach (string symbol in symbols) { Price cachedPrice = cache.GetPrice(symbol, date); Assert.IsNotNull(cachedPrice); } } #endregion #region Cache State Tests [TestMethod] public void ContainsPrice_ShouldReturnTrueForExistingPrice() { string symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); Price price = CreatePrice(symbol, date, 250); cache.Add(price); Assert.IsTrue(cache.ContainsPrice(symbol, date)); } [TestMethod] public void ContainsPrice_ShouldReturnFalseForNonExistingPrice() { string symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); Assert.IsFalse(cache.ContainsPrice(symbol, date)); } [TestMethod] public void GetMinCacheDate_ShouldReturnEpochIfNotPresent() { DateTime minDate = cache.GetMinCacheDate("UNKNOWN"); Assert.AreEqual(DateTime.MinValue, minDate); // Utility.Epoch assumed to be DateTime.MinValue } [TestMethod] public void GetMinCacheDate_ShouldReturnCorrectMinDateForExistingSymbol() { string symbol = "MSFT"; DateTime date1 = new DateTime(2026, 2, 22); DateTime date2 = new DateTime(2026, 2, 23); cache.Add(CreatePrice(symbol, date1, 250)); cache.Add(CreatePrice(symbol, date2, 260)); DateTime minDate = cache.GetMinCacheDate(symbol); Assert.AreEqual(date1, minDate); } #endregion #region Concurrency & Thread Safety Tests [TestMethod] public void AddPricesConcurrently_ShouldNotThrowException() { string symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); List prices = new List { CreatePrice(symbol, date, 250), CreatePrice(symbol, date.AddDays(1), 260) }; List tasks = new List(); foreach (Price price in prices) { tasks.Add(System.Threading.Tasks.Task.Run(() => cache.Add(price))); } System.Threading.Tasks.Task.WhenAll(tasks).Wait(); Price cachedPrice = cache.GetPrice(symbol, date); Assert.IsNotNull(cachedPrice); } [TestMethod] public void ThreadSafety_WithMultipleAdds_ShouldNotCorruptCache() { string symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); List prices = new List(); for (int i = 0; i < 1000; i++) { prices.Add(CreatePrice(symbol, date.AddDays(i), 250 + i)); } List tasks = new List(); foreach (Price price in prices) { tasks.Add(System.Threading.Tasks.Task.Run(() => cache.Add(price))); } System.Threading.Tasks.Task.WhenAll(tasks).Wait(); Price cachedPrice = cache.GetPrice(symbol, date.AddDays(500)); Assert.IsNotNull(cachedPrice); } #endregion #region Remove/Clear Tests [TestMethod] public void RemoveDate_ShouldRemovePrice() { string symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); cache.Add(CreatePrice(symbol, date, 250)); cache.RemoveDate(date); Assert.IsFalse(cache.ContainsPrice(symbol, date)); } [TestMethod] public void Clear_ShouldRemoveAllPrices() { string symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); cache.Add(CreatePrice(symbol, date, 250)); cache.Clear(); Assert.IsFalse(cache.ContainsPrice(symbol, date)); } #endregion #region Date & Range Tests [TestMethod] public void GetPrices_ShouldReturnCorrectPricesInRange() { string symbol = "MSFT"; DateTime endDate = new DateTime(2026, 2, 25); int dayCount = 3; DateGenerator dateGenerator = new DateGenerator(); List historicalDates = dateGenerator.GenerateHistoricalDates(endDate, dayCount); Prices prices = new Prices(); int priceValue = 250; foreach (DateTime date in historicalDates) { prices.Add(CreatePrice(symbol, date, priceValue)); priceValue++; } cache.Add(prices); Prices rangePrices = cache.GetPrices(symbol, endDate, dayCount); Assert.AreEqual(historicalDates.Count, rangePrices.Count); for (int i = 0; i < historicalDates.Count; i++) { Assert.AreEqual(historicalDates[i], rangePrices[i].Date); } } [TestMethod] public void GetPrices_ShouldReturnEmptyForNoPricesInRange() { string symbol = "MSFT"; DateTime startDate = new DateTime(2026, 2, 22); Prices rangePrices = cache.GetPrices(symbol, startDate, 3); Assert.AreEqual(0, rangePrices.Count); } #endregion #region Duplicate & Edge Case Tests [TestMethod] public void AddDuplicatePrice_ShouldNotDuplicateInCache() { string symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); Price price1 = CreatePrice(symbol, date, 250); Price price2 = CreatePrice(symbol, date, 250); cache.Add(price1); cache.Add(price2); Price cachedPrice = cache.GetPrice(symbol, date); Assert.IsNotNull(cachedPrice); Assert.AreEqual(250, cachedPrice.Close); } [TestMethod] public void RemoveNonExistentPrice_ShouldNotThrowException() { DateTime date = new DateTime(2026, 2, 22); cache.RemoveDate(date); Assert.IsTrue(true); } [TestMethod] public void AddPortfolioTrades_WithNoNewPrices_ShouldNotFetch() { DateGenerator dateGenerator = new DateGenerator(); string symbol = "MSFT"; DateTime tradeDate = new DateTime(2026, 2, 22); tradeDate = dateGenerator.GetPrevBusinessDay(tradeDate); // Add a price to the cache cache.Add(CreatePrice(symbol, tradeDate, 250)); // Create PortfolioTrades manually PortfolioTrades trades = new PortfolioTrades(); trades.Add(new PortfolioTrade { Symbol = symbol, TradeDate = tradeDate, Shares = 1, Price = 250, Status = "OPEN" }); // Add portfolio trades to the cache — should not fetch additional prices cache.Add(trades); // Retrieve prices for the generated historical dates Prices cachedPrices = cache.GetPrices(symbol, tradeDate, 1); Assert.AreEqual(1, cachedPrices.Count); Assert.AreEqual(tradeDate, cachedPrices[0].Date); Assert.AreEqual(250, cachedPrices[0].Close); } [TestMethod] public void AddListOfSymbols_EmptyList_ShouldNotThrow() { cache.Add(new List(), DateTime.Today); Assert.IsTrue(true); } [TestMethod] public void AddListOfSymbols_PriceIsNull_ShouldNotThrow() { List symbols = new List { "MSFT" }; cache.Add(symbols, new DateTime(1900, 1, 1)); Assert.IsTrue(true); } [TestMethod] public void ContainsPrice_ListSymbols_NullOrEmpty_ShouldReturnFalse() { Assert.IsFalse(cache.ContainsPrice((List)null, DateTime.Today)); Assert.IsFalse(cache.ContainsPrice(new List(), DateTime.Today)); } [TestMethod] public void GetPrice_SymbolExistsButDateMissing_ShouldReturnNull() { string symbol = "MSFT"; cache.Add(CreatePrice(symbol, new DateTime(2026, 2, 22), 250)); Price result = cache.GetPrice(symbol, new DateTime(2026, 2, 23)); Assert.IsNull(result); } [TestMethod] public void RemoveDate_EmptyCache_ShouldNotThrow() { cache.RemoveDate(DateTime.Today); Assert.IsTrue(true); } [TestMethod] public void Refresh_EmptyCache_ShouldNotThrow() { cache.Clear(); cache.Refresh(); Assert.IsTrue(true); } [TestMethod] public void Dispose_WhenAlreadyDisposed_ShouldNotThrow() { cache.Dispose(); cache.Dispose(); Assert.IsTrue(true); } [TestMethod] public void Refresh_WithExistingPrices_ShouldNotThrow() { DateGenerator dateGenerator = new DateGenerator(); String symbol = "MSFT"; DateTime date1 = new DateTime(2026, 2, 22); DateTime date2 = new DateTime(2026, 2, 23); date1=dateGenerator.FindPrevBusinessDay(date1); date2=dateGenerator.FindNextBusinessDay(date2); Price price1 = CreatePrice(symbol, date1, 250); Price price2 = CreatePrice(symbol, date2, 260); cache.Add(price1); cache.Add(price2); // Refresh should process existing prices without throwing cache.Refresh(); Prices cachedPrices1 = cache.GetPrices(symbol, date1, 1); Prices cachedPrices2 = cache.GetPrices(symbol, date2, 1); Assert.AreEqual(1, cachedPrices1.Count); Assert.AreEqual(1, cachedPrices2.Count); } [TestMethod] public void AddPrices_NullOrEmpty_ShouldNotThrow() { Prices emptyPrices = new Prices(); cache.Add(emptyPrices); // Should not throw Prices emptyList = new Prices(); cache.Add(emptyList); // Should not throw cache.Add((Price)null!); // Should not throw Assert.IsTrue(true); } [TestMethod] public void Dispose_WithPopulatedCache_ShouldNotThrow() { String symbol = "MSFT"; DateTime date = new DateTime(2026, 2, 22); Price price = CreatePrice(symbol, date, 250); cache.Add(price); // Dispose should handle non-empty cache gracefully cache.Dispose(); // Subsequent calls to Dispose should still be safe cache.Dispose(); Assert.IsTrue(true); } [TestMethod] public void ThreadSafety_WithMultipleAdds_ShouldNotCorruptCache_BusinessDays() { string symbol = "MSFT"; DateGenerator dateGenerator = new DateGenerator(); DateTime baseDate = new DateTime(2026, 2, 22); int numPrices = 1000; List prices = new List(); HashSet usedDates = new HashSet(); DateTime currentDate = baseDate; int priceCounter = 0; while (prices.Count < numPrices) { // Advance one day currentDate = currentDate.AddDays(1); // Adjust to previous business day DateTime businessDate = dateGenerator.GetPrevBusinessDay(currentDate); // Only add if unique if (!usedDates.Contains(businessDate)) { usedDates.Add(businessDate); prices.Add(CreatePrice(symbol, businessDate, 250 + priceCounter)); priceCounter++; currentDate = businessDate; // continue from this business date } } // Multi-threaded addition List tasks = new List(); foreach (Price price in prices) { tasks.Add(System.Threading.Tasks.Task.Run(() => cache.Add(price))); } System.Threading.Tasks.Task.WhenAll(tasks).Wait(); // Pick a middle sample DateTime sampleDate = prices[numPrices / 2].Date; Price cachedPrice = cache.GetPrice(symbol, sampleDate); Assert.IsNotNull(cachedPrice); Assert.AreEqual(250 + (numPrices / 2), cachedPrice.Close); // Verify total count Prices allCachedPrices = cache.GetPrices(symbol, prices[numPrices - 1].Date, numPrices); Assert.AreEqual(numPrices, allCachedPrices.Count); } #endregion }