using CartWise.Application.DTOs; 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 ProductServiceTests : IDisposable { private static readonly DateTime Now = new(2026, 8, 10, 12, 0, 0, DateTimeKind.Utc); private readonly CartWiseDbContext _db; private readonly ProductService _sut; public ProductServiceTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; _db = new CartWiseDbContext(options); _sut = new ProductService(_db, [], TimeProvider.System); } public void Dispose() { _db.Dispose(); GC.SuppressFinalize(this); } [Fact] public async Task SearchAsync_ReturnsMatchingProductsWithBrandName() { var brand = new Brand("Jif"); var product = new Product("Jif Creamy Peanut Butter", ProductSourceType.Manual, Now, brandId: brand.BrandId, sizeValue: 16m, sizeUnit: "oz"); _db.Brands.Add(brand); _db.Products.Add(product); await _db.SaveChangesAsync(); var results = await _sut.SearchAsync("peanut"); var result = Assert.Single(results); Assert.Equal("Jif Creamy Peanut Butter", result.Name); Assert.Equal("Jif", result.BrandName); Assert.Equal("16 oz", result.PackageDisplay); } [Fact] public async Task SearchAsync_ReturnsEmptyForBlankQuery() { var results = await _sut.SearchAsync(" "); Assert.Empty(results); } [Fact] public async Task SearchAsync_ReturnsEmptyWhenNothingMatches() { _db.Products.Add(new Product("Whole Milk", ProductSourceType.Manual, Now)); await _db.SaveChangesAsync(); var results = await _sut.SearchAsync("cereal"); Assert.Empty(results); } [Fact] public async Task GetDetailsAsync_ReturnsNullWhenProductDoesNotExist() { var details = await _sut.GetDetailsAsync(Guid.NewGuid()); Assert.Null(details); } [Fact] public async Task GetDetailsAsync_ReturnsBrandConceptAndIdentifiers() { var brand = new Brand("Jif"); var concept = new GroceryConcept("Peanut Butter", Now); var product = new Product("Jif Creamy 16 oz", ProductSourceType.Manual, Now, groceryConceptId: concept.GroceryConceptId, brandId: brand.BrandId); product.AddIdentifier(ProductIdentifierType.UpcA, "051500255516"); _db.Brands.Add(brand); _db.GroceryConcepts.Add(concept); _db.Products.Add(product); await _db.SaveChangesAsync(); var details = await _sut.GetDetailsAsync(product.ProductId); Assert.NotNull(details); Assert.Equal("Jif", details!.BrandName); Assert.Equal("Peanut Butter", details.ConceptName); Assert.Equal("051500255516", Assert.Single(details.BarcodeIdentifiers)); } [Fact] public async Task ResolveBarcodeAsync_ReturnsLocalProductWithoutCallingProvider() { var product = new Product("Whole Milk", ProductSourceType.Manual, Now); product.AddIdentifier(ProductIdentifierType.UpcA, "111111111111"); _db.Products.Add(product); await _db.SaveChangesAsync(); var sut = new ProductService(_db, [], TimeProvider.System); var result = await sut.ResolveBarcodeAsync("111111111111"); Assert.True(result.IsSuccess); Assert.Equal("Whole Milk", result.Value!.Name); } [Fact] public async Task ResolveBarcodeAsync_FallsBackToProviderAndPersistsResult() { var provider = new FakeProductDataProvider(new Dictionary { ["222222222222"] = new ProductLookupResult { Name = "Imported Cereal", BrandName = "Acme", SizeValue = 12m, SizeUnit = "oz", SourceExternalId = "off-123" } }); var sut = new ProductService(_db, [provider], TimeProvider.System); var result = await sut.ResolveBarcodeAsync("222222222222"); Assert.True(result.IsSuccess); Assert.Equal("Imported Cereal", result.Value!.Name); Assert.Equal("Acme", result.Value.BrandName); var persisted = await _db.Products.AsNoTracking().SingleAsync(); Assert.Equal(ProductSourceType.OpenFoodFacts, persisted.SourceType); Assert.Equal("off-123", persisted.SourceExternalId); } [Fact] public async Task ResolveBarcodeAsync_FailsWhenNoLocalMatchAndNoProviderResult() { var provider = new FakeProductDataProvider(new Dictionary()); var sut = new ProductService(_db, [provider], TimeProvider.System); var result = await sut.ResolveBarcodeAsync("999999999999"); Assert.False(result.IsSuccess); } [Fact] public async Task ResolveBarcodeAsync_FailsWhenBarcodeIsMissing() { var result = await _sut.ResolveBarcodeAsync(" "); Assert.False(result.IsSuccess); } }