using CartWise.Application.Services; using CartWise.Domain.Entities; using CartWise.Domain.Enums; using CartWise.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace CartWise.Application.Tests; public class PriceServiceTests : IDisposable { private static readonly DateTime Now = new(2026, 8, 10, 12, 0, 0, DateTimeKind.Utc); private readonly CartWiseDbContext _db; private readonly PriceService _sut; public PriceServiceTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; _db = new CartWiseDbContext(options); _sut = new PriceService(_db, TimeProvider.System); } public void Dispose() { _db.Dispose(); GC.SuppressFinalize(this); } [Fact] public async Task GetHouseholdPriceInsightAsync_ReturnsNullWhenNoObservationsExist() { var insight = await _sut.GetHouseholdPriceInsightAsync(Guid.NewGuid(), Guid.NewGuid()); Assert.Null(insight); } [Fact] public async Task RecordObservationAsync_ThenGetInsight_ReturnsLatestAverageMedianAndLowest() { var householdId = Guid.NewGuid(); var productId = Guid.NewGuid(); await _sut.RecordObservationAsync(productId, 3.00m, Now.AddDays(-2), PriceObservationSourceType.UserEntered, 1m, householdId: householdId); await _sut.RecordObservationAsync(productId, 5.00m, Now.AddDays(-1), PriceObservationSourceType.UserEntered, 1m, householdId: householdId); await _sut.RecordObservationAsync(productId, 4.00m, Now, PriceObservationSourceType.UserEntered, 1m, householdId: householdId); var insight = await _sut.GetHouseholdPriceInsightAsync(householdId, productId); Assert.NotNull(insight); Assert.Equal(3, insight!.ObservationCount); Assert.Equal(4.00m, insight.LatestPrice); Assert.Equal(Now, insight.LatestObservedUtc); Assert.Equal(4.00m, insight.AveragePrice); Assert.Equal(4.00m, insight.MedianPrice); Assert.Equal(3.00m, insight.LowestPrice); } [Fact] public async Task GetHouseholdPriceInsightAsync_ComputesMedianForEvenCount() { var householdId = Guid.NewGuid(); var productId = Guid.NewGuid(); await _sut.RecordObservationAsync(productId, 2.00m, Now.AddDays(-1), PriceObservationSourceType.UserEntered, 1m, householdId: householdId); await _sut.RecordObservationAsync(productId, 4.00m, Now, PriceObservationSourceType.UserEntered, 1m, householdId: householdId); var insight = await _sut.GetHouseholdPriceInsightAsync(householdId, productId); Assert.Equal(3.00m, insight!.MedianPrice); } [Fact] public async Task GetHouseholdPriceInsightAsync_DoesNotMixDifferentHouseholds() { var productId = Guid.NewGuid(); await _sut.RecordObservationAsync(productId, 3.00m, Now, PriceObservationSourceType.UserEntered, 1m, householdId: Guid.NewGuid()); var insight = await _sut.GetHouseholdPriceInsightAsync(Guid.NewGuid(), productId); Assert.Null(insight); } [Fact] public async Task GetHouseholdPriceHistoryAsync_ReturnsNewestFirst() { var householdId = Guid.NewGuid(); var productId = Guid.NewGuid(); await _sut.RecordObservationAsync(productId, 3.00m, Now.AddDays(-1), PriceObservationSourceType.UserEntered, 1m, householdId: householdId); await _sut.RecordObservationAsync(productId, 4.00m, Now, PriceObservationSourceType.UserEntered, 1m, householdId: householdId); var history = await _sut.GetHouseholdPriceHistoryAsync(householdId, productId); Assert.Equal(2, history.Count); Assert.Equal(4.00m, history[0].Price); Assert.Equal(3.00m, history[1].Price); } [Fact] public async Task GetHouseholdPricedProductsAsync_ReturnsEmptyWhenNoObservationsExist() { var products = await _sut.GetHouseholdPricedProductsAsync(Guid.NewGuid()); Assert.Empty(products); } [Fact] public async Task GetHouseholdPricedProductsAsync_GroupsByProductAndSortsByName() { var householdId = Guid.NewGuid(); var milk = new Product("Milk", ProductSourceType.Manual, Now); var bread = new Product("Bread", ProductSourceType.Manual, Now); _db.Products.Add(milk); _db.Products.Add(bread); await _db.SaveChangesAsync(); await _sut.RecordObservationAsync(milk.ProductId, 3.00m, Now.AddDays(-1), PriceObservationSourceType.UserEntered, 1m, householdId: householdId); await _sut.RecordObservationAsync(milk.ProductId, 4.00m, Now, PriceObservationSourceType.UserEntered, 1m, householdId: householdId); await _sut.RecordObservationAsync(bread.ProductId, 2.50m, Now, PriceObservationSourceType.UserEntered, 1m, householdId: householdId); var products = await _sut.GetHouseholdPricedProductsAsync(householdId); Assert.Equal(2, products.Count); Assert.Equal("Bread", products[0].ProductName); Assert.Equal("Milk", products[1].ProductName); Assert.Equal(2, products[1].ObservationCount); Assert.Equal(4.00m, products[1].LatestPrice); Assert.Equal(3.50m, products[1].AveragePrice); Assert.Equal(3.00m, products[1].LowestPrice); } [Fact] public async Task GetHouseholdPricedProductsAsync_DoesNotMixDifferentHouseholds() { var milk = new Product("Milk", ProductSourceType.Manual, Now); _db.Products.Add(milk); await _db.SaveChangesAsync(); await _sut.RecordObservationAsync(milk.ProductId, 3.00m, Now, PriceObservationSourceType.UserEntered, 1m, householdId: Guid.NewGuid()); var products = await _sut.GetHouseholdPricedProductsAsync(Guid.NewGuid()); Assert.Empty(products); } }