using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.Items; using Xunit; namespace NeonSprawl.Server.Tests.Game.Items; public sealed class PlayerInventoryOperationsTests { [Fact] public void TryAddStack_ToEmptyBag_ShouldPlaceInFirstSlot() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); // Act var outcome = PlayerInventoryOperations.TryAddStack( "dev-local-1", "scrap_metal_bulk", quantity: 10, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind); Assert.NotNull(outcome.Snapshot); Assert.Equal("scrap_metal_bulk", outcome.Snapshot!.BagSlots[0].ItemId); Assert.Equal(10, outcome.Snapshot.BagSlots[0].Quantity); } [Fact] public void TryAddStack_ShouldMergeIntoPartialStackRespectingStackMax() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); _ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 15, registry, store); // Act var outcome = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 5, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind); Assert.Equal(20, CountItem(outcome.Snapshot!, "field_stim_mk0")); Assert.Equal(1, OccupiedBagSlotCount(outcome.Snapshot!)); } [Fact] public void TryAddStack_WhenBagFull_ShouldDenyWithInventoryFull() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++) { var add = PlayerInventoryOperations.TryAddStack( "dev-local-1", "survey_drone_kit", quantity: 1, registry, store); Assert.Equal(PlayerInventoryMutationKind.Applied, add.Kind); } store.TryGetSnapshot("dev-local-1", out var before); // Act var outcome = PlayerInventoryOperations.TryAddStack( "dev-local-1", "scrap_metal_bulk", quantity: 1, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind); Assert.Equal(PlayerInventoryReasonCodes.InventoryFull, outcome.ReasonCode); store.TryGetSnapshot("dev-local-1", out var after); Assert.Equal(TotalBagQuantity(before!), TotalBagQuantity(after!)); } [Fact] public void TryAddStack_ToEquipment_ShouldUseEquipmentContainer() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); // Act var outcome = PlayerInventoryOperations.TryAddStack( "dev-local-1", "prototype_armor_shell", quantity: 1, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind); Assert.Equal("prototype_armor_shell", outcome.Snapshot!.EquipmentSlots[0].ItemId); Assert.Equal(1, outcome.Snapshot.EquipmentSlots[0].Quantity); Assert.All(outcome.Snapshot.BagSlots, static s => Assert.True(s.IsEmpty)); } [Fact] public void TryAddStack_WhenEquipmentOccupied_ShouldDenyWithInventoryFull() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); _ = PlayerInventoryOperations.TryAddStack( "dev-local-1", "prototype_armor_shell", quantity: 1, registry, store); // Act var outcome = PlayerInventoryOperations.TryAddStack( "dev-local-1", "prototype_armor_shell", quantity: 1, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind); Assert.Equal(PlayerInventoryReasonCodes.InventoryFull, outcome.ReasonCode); } [Fact] public void TryAddStack_ForUnknownItem_ShouldDenyWithInvalidItem() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); // Act var outcome = PlayerInventoryOperations.TryAddStack( "dev-local-1", "nonexistent_item_id", quantity: 1, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind); Assert.Equal(PlayerInventoryReasonCodes.InvalidItem, outcome.ReasonCode); } [Fact] public void TryAddStack_ForNonPositiveQuantity_ShouldDenyWithInvalidQuantity() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); // Act var outcome = PlayerInventoryOperations.TryAddStack( "dev-local-1", "scrap_metal_bulk", quantity: 0, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind); Assert.Equal(PlayerInventoryReasonCodes.InvalidQuantity, outcome.ReasonCode); } [Fact] public void TryRemoveStack_ShouldDrainLowestSlotIndexFirst() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); _ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 20, registry, store); _ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 5, registry, store); // Act var outcome = PlayerInventoryOperations.TryRemoveStack( "dev-local-1", "field_stim_mk0", quantity: 20, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind); Assert.True(outcome.Snapshot!.BagSlots[0].IsEmpty); Assert.Equal("field_stim_mk0", outcome.Snapshot.BagSlots[1].ItemId); Assert.Equal(5, outcome.Snapshot.BagSlots[1].Quantity); } [Fact] public void TryRemoveStack_WhenInsufficientQuantity_ShouldDenyWithoutMutation() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); _ = PlayerInventoryOperations.TryAddStack("dev-local-1", "scrap_metal_bulk", 5, registry, store); store.TryGetSnapshot("dev-local-1", out var before); // Act var outcome = PlayerInventoryOperations.TryRemoveStack( "dev-local-1", "scrap_metal_bulk", quantity: 6, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind); Assert.Equal(PlayerInventoryReasonCodes.InsufficientQuantity, outcome.ReasonCode); store.TryGetSnapshot("dev-local-1", out var after); Assert.Equal(TotalBagQuantity(before!), TotalBagQuantity(after!)); } [Fact] public void TryAddStack_WhenQuantityExceedsStackMax_ShouldSpanMultipleEmptySlots() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); // Act var outcome = PlayerInventoryOperations.TryAddStack( "dev-local-1", "field_stim_mk0", quantity: 41, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind); Assert.Equal(41, CountItem(outcome.Snapshot!, "field_stim_mk0")); Assert.Equal(3, OccupiedBagSlotCount(outcome.Snapshot!)); Assert.Equal(20, outcome.Snapshot!.BagSlots[0].Quantity); Assert.Equal(20, outcome.Snapshot.BagSlots[1].Quantity); Assert.Equal(1, outcome.Snapshot.BagSlots[2].Quantity); } [Fact] public void TryAddStack_ForUnknownPlayer_ShouldReturnStoreMissing() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); // Act var outcome = PlayerInventoryOperations.TryAddStack( "unknown-player-xyz", "scrap_metal_bulk", quantity: 1, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.StoreMissing, outcome.Kind); Assert.Null(outcome.ReasonCode); Assert.Null(outcome.Snapshot); } [Fact] public void TryAddStack_ForUnknownPlayer_WithInvalidQuantity_ShouldDenyWithReasonCode() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); // Act var outcome = PlayerInventoryOperations.TryAddStack( "unknown-player-xyz", "scrap_metal_bulk", quantity: 0, registry, store); // Assert Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind); Assert.Equal(PlayerInventoryReasonCodes.InvalidQuantity, outcome.ReasonCode); Assert.Null(outcome.Snapshot); } [Fact] public void TrySimulateAddStack_WhenBagFullForBagItem_ShouldReturnFalse() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++) { _ = PlayerInventoryOperations.TryAddStack( "dev-local-1", "survey_drone_kit", quantity: 1, registry, store); } store.TryGetSnapshot("dev-local-1", out var snapshot); var simulated = new PlayerInventorySnapshot { BagSlots = PlayerInventorySnapshot.CloneSlots(snapshot!.BagSlots), EquipmentSlots = PlayerInventorySnapshot.CloneSlots(snapshot.EquipmentSlots), }; // Act var canPlace = PlayerInventoryOperations.TrySimulateAddStack( ref simulated, "refined_plate_stock", quantity: 1, registry); // Assert Assert.False(canPlace); } [Fact] public void TrySimulateAddStack_WhenBagFullButEquipmentRouteAvailable_ShouldReturnTrue() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var registry = factory.Services.GetRequiredService(); var store = factory.Services.GetRequiredService(); for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++) { _ = PlayerInventoryOperations.TryAddStack( "dev-local-1", "survey_drone_kit", quantity: 1, registry, store); } store.TryGetSnapshot("dev-local-1", out var snapshot); var simulated = new PlayerInventorySnapshot { BagSlots = PlayerInventorySnapshot.CloneSlots(snapshot!.BagSlots), EquipmentSlots = PlayerInventorySnapshot.CloneSlots(snapshot.EquipmentSlots), }; // Act var canPlace = PlayerInventoryOperations.TrySimulateAddStack( ref simulated, "prototype_armor_shell", quantity: 1, registry); // Assert Assert.True(canPlace); Assert.Equal("prototype_armor_shell", simulated.EquipmentSlots[0].ItemId); Assert.Equal(1, simulated.EquipmentSlots[0].Quantity); } private static int CountItem(PlayerInventorySnapshot snapshot, string itemId) { var total = 0; foreach (var slot in snapshot.BagSlots) { if (!slot.IsEmpty && slot.ItemId == itemId) { total += slot.Quantity; } } return total; } private static int OccupiedBagSlotCount(PlayerInventorySnapshot snapshot) { var count = 0; foreach (var slot in snapshot.BagSlots) { if (!slot.IsEmpty) { count++; } } return count; } private static int TotalBagQuantity(PlayerInventorySnapshot snapshot) { var total = 0; foreach (var slot in snapshot.BagSlots) { total += slot.Quantity; } return total; } }