using FluentAssertions; using LittleShop.Data; using LittleShop.DTOs; using LittleShop.Models; using LittleShop.Services; using LittleShop.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Hosting; using Moq; using Xunit; namespace LittleShop.Tests.Unit; public class ProductMultiBuyServiceTests : IDisposable { private readonly LittleShopContext _context; private readonly IProductService _productService; private readonly Mock _mockEnvironment; public ProductMultiBuyServiceTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; _context = new LittleShopContext(options); _mockEnvironment = new Mock(); _mockEnvironment.Setup(e => e.WebRootPath).Returns("/test/wwwroot"); _productService = new ProductService(_context, _mockEnvironment.Object); } [Fact] public async Task CreateProductMultiBuy_ShouldCreateSuccessfully() { // Arrange var product = await CreateTestProduct(); var createDto = new CreateProductMultiBuyDto { ProductId = product.Id, Name = "3 for £25", Description = "Save £5 when you buy 3", Quantity = 3, Price = 25.00m }; // Act var result = await _productService.CreateProductMultiBuyAsync(createDto); // Assert result.Should().NotBeNull(); result.Name.Should().Be("3 for £25"); result.Quantity.Should().Be(3); result.Price.Should().Be(25.00m); result.PricePerUnit.Should().BeApproximately(8.33m, 0.01m); } [Fact] public async Task CreateProductMultiBuy_DuplicateQuantity_ShouldThrow() { // Arrange var product = await CreateTestProduct(); // Create first multi-buy await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto { ProductId = product.Id, Name = "Twin Pack", Quantity = 2, Price = 19.00m }); // Try to create duplicate quantity var duplicateDto = new CreateProductMultiBuyDto { ProductId = product.Id, Name = "Double Deal", Quantity = 2, // Same quantity Price = 18.00m }; // Act & Assert var act = async () => await _productService.CreateProductMultiBuyAsync(duplicateDto); await act.Should().ThrowAsync() .WithMessage("*already exists*"); } [Fact] public async Task GetProductWithMultiBuys_ShouldReturnAllMultiBuys() { // Arrange var product = await CreateTestProduct(); // Create multiple multi-buys await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto { ProductId = product.Id, Name = "Twin Pack", Quantity = 2, Price = 19.00m }); await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto { ProductId = product.Id, Name = "Triple Pack", Quantity = 3, Price = 25.00m }); // Act var result = await _productService.GetProductByIdAsync(product.Id); // Assert result.Should().NotBeNull(); result!.MultiBuys.Should().HaveCount(2); result.MultiBuys.Should().Contain(mb => mb.Name == "Twin Pack"); result.MultiBuys.Should().Contain(mb => mb.Name == "Triple Pack"); result.MultiBuys.Should().BeInAscendingOrder(mb => mb.Quantity); } [Fact] public async Task UpdateProductMultiBuy_ShouldUpdateSuccessfully() { // Arrange var product = await CreateTestProduct(); var multiBuy = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto { ProductId = product.Id, Name = "Original Name", Quantity = 2, Price = 20.00m }); var updateDto = new UpdateProductMultiBuyDto { Name = "Updated Name", Description = "New description", Price = 18.00m, SortOrder = 10, IsActive = false }; // Act var success = await _productService.UpdateProductMultiBuyAsync(multiBuy.Id, updateDto); // Assert success.Should().BeTrue(); // Get the updated multi-buy to verify changes var updated = await _productService.GetProductMultiBuyByIdAsync(multiBuy.Id); updated.Should().NotBeNull(); updated!.Name.Should().Be("Updated Name"); updated.Description.Should().Be("New description"); updated.Price.Should().Be(18.00m); updated.PricePerUnit.Should().Be(9.00m); updated.SortOrder.Should().Be(10); updated.IsActive.Should().BeFalse(); } [Fact] public async Task DeleteProductMultiBuy_ShouldSoftDelete() { // Arrange var product = await CreateTestProduct(); var multiBuy = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto { ProductId = product.Id, Name = "To Delete", Quantity = 2, Price = 20.00m }); // Act await _productService.DeleteProductMultiBuyAsync(multiBuy.Id); // Assert var deletedMultiBuy = await _context.ProductMultiBuys .IgnoreQueryFilters() .FirstOrDefaultAsync(mb => mb.Id == multiBuy.Id); deletedMultiBuy.Should().NotBeNull(); deletedMultiBuy!.IsActive.Should().BeFalse(); } [Fact] public async Task PricePerUnit_ShouldCalculateCorrectly() { // Arrange & Act var product = await CreateTestProduct(); var singleItem = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto { ProductId = product.Id, Name = "Single", Quantity = 1, Price = 10.00m }); var twinPack = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto { ProductId = product.Id, Name = "Twin Pack", Quantity = 2, Price = 19.00m }); var triplePack = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto { ProductId = product.Id, Name = "Triple Pack", Quantity = 3, Price = 25.00m }); // Assert singleItem.PricePerUnit.Should().Be(10.00m); twinPack.PricePerUnit.Should().Be(9.50m); triplePack.PricePerUnit.Should().BeApproximately(8.33m, 0.01m); } private async Task CreateTestProduct() { var category = new Category { Id = Guid.NewGuid(), Name = "Test Category", IsActive = true }; _context.Categories.Add(category); var product = new Product { Id = Guid.NewGuid(), Name = "Test Product", Price = 10.00m, CategoryId = category.Id, IsActive = true, Weight = 1.0m, WeightUnit = ProductWeightUnit.Kilograms }; _context.Products.Add(product); await _context.SaveChangesAsync(); return product; } public void Dispose() { _context.Dispose(); } }