using System.Linq;
using System.Net;
using System.Net.Http.Json;
using NeonSprawl.Server.Game.Gathering;
using NeonSprawl.Server.Game.Interaction;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Game.World;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Interaction;
/// NEO-63: resource_node interact delegates to .
public sealed class InteractionResourceNodeGatherIntegrationTests
{
[Fact]
public async Task PostInteract_ResourceNode_WhenInRange_ShouldGrantInventoryAndSalvageXp()
{
// Arrange — node at (12, -6), radius 3; stand at (10, -6) → horizontal distance 2
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
await MoveNearAlphaAsync(client);
// Act
var interact = await client.PostAsJsonAsync(
"/game/players/dev-local-1/interact",
new InteractionRequest
{
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
});
var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");
// Assert
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
var envelope = await interact.Content.ReadFromJsonAsync();
Assert.NotNull(envelope);
Assert.True(envelope!.Allowed);
Assert.Equal(PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, envelope.InteractableId);
Assert.Equal(HttpStatusCode.OK, inventory.StatusCode);
var bag = await inventory.Content.ReadFromJsonAsync();
Assert.NotNull(bag);
Assert.Equal(1, CountItem(bag!, "scrap_metal_bulk"));
Assert.Equal(HttpStatusCode.OK, progression.StatusCode);
var skills = await progression.Content.ReadFromJsonAsync();
Assert.NotNull(skills);
var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
Assert.Equal(GatherSkillXpConstants.ActivityXpPerResourceNodeGather, salvage.Xp);
}
[Fact]
public async Task PostInteract_ResourceNode_WhenDepleted_ShouldDenyWithNodeDepletedWithoutGrants()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
await MoveNearAlphaAsync(client);
var request = new InteractionRequest
{
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
};
// Act — exhaust capacity, then one more interact
var successfulGathers = new InteractionResponse[10];
for (var i = 0; i < successfulGathers.Length; i++)
{
var gather = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", request);
successfulGathers[i] = (await gather.Content.ReadFromJsonAsync())!;
}
var depletedInteract = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", request);
var inventoryAfter = await client.GetAsync("/game/players/dev-local-1/inventory");
var progressionAfter = await client.GetAsync("/game/players/dev-local-1/skill-progression");
// Assert
Assert.All(successfulGathers, static r => Assert.True(r.Allowed));
Assert.Equal(HttpStatusCode.OK, depletedInteract.StatusCode);
var envelope = await depletedInteract.Content.ReadFromJsonAsync();
Assert.NotNull(envelope);
Assert.False(envelope!.Allowed);
Assert.Equal(GatherReasonCodes.NodeDepleted, envelope.ReasonCode);
Assert.Equal(10, CountItem(
(await inventoryAfter.Content.ReadFromJsonAsync())!,
"scrap_metal_bulk"));
Assert.Equal(
10 * GatherSkillXpConstants.ActivityXpPerResourceNodeGather,
(await progressionAfter.Content.ReadFromJsonAsync())!
.Skills!.Single(static s => s.Id == "salvage").Xp);
}
[Fact]
public async Task PostInteract_ResourceNode_WhenBagFull_ShouldDenyWithInventoryFullWithoutDepletingNode()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
{
await client.PostAsJsonAsync(
"/game/players/dev-local-1/inventory",
new PlayerInventoryMutationRequest
{
SchemaVersion = PlayerInventoryMutationRequest.CurrentSchemaVersion,
MutationKind = "add",
ItemId = "survey_drone_kit",
Quantity = 1,
});
}
await MoveNearAlphaAsync(client);
var inventoryBefore = await client.GetAsync("/game/players/dev-local-1/inventory");
var progressionBefore = await client.GetAsync("/game/players/dev-local-1/skill-progression");
var scrapBefore = CountItem(
(await inventoryBefore.Content.ReadFromJsonAsync())!,
"scrap_metal_bulk");
var salvageBefore = (await progressionBefore.Content.ReadFromJsonAsync())!
.Skills!.Single(static s => s.Id == "salvage").Xp;
// Act
var interact = await client.PostAsJsonAsync(
"/game/players/dev-local-1/interact",
new InteractionRequest
{
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
});
// Assert
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
var envelope = await interact.Content.ReadFromJsonAsync();
Assert.NotNull(envelope);
Assert.False(envelope!.Allowed);
Assert.Equal(GatherReasonCodes.InventoryFull, envelope.ReasonCode);
Assert.Equal(
scrapBefore,
CountItem(
(await (await client.GetAsync("/game/players/dev-local-1/inventory")).Content
.ReadFromJsonAsync())!,
"scrap_metal_bulk"));
Assert.Equal(
salvageBefore,
(await (await client.GetAsync("/game/players/dev-local-1/skill-progression")).Content
.ReadFromJsonAsync())!
.Skills!.Single(static s => s.Id == "salvage").Xp);
}
[Fact]
public async Task PostInteract_PrototypeTerminal_ShouldNotGrantInventoryOrSalvageXp()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var move = new MoveCommandRequest
{
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector { X = 0.5, Y = 0.9, Z = 0.5 },
};
await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);
// Act
var interact = await client.PostAsJsonAsync(
"/game/players/dev-local-1/interact",
new InteractionRequest
{
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId,
});
var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");
// Assert
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
var envelope = await interact.Content.ReadFromJsonAsync();
Assert.NotNull(envelope);
Assert.True(envelope!.Allowed);
var bag = await inventory.Content.ReadFromJsonAsync();
Assert.NotNull(bag);
Assert.Equal(0, CountItem(bag!, "scrap_metal_bulk"));
var skills = await progression.Content.ReadFromJsonAsync();
Assert.NotNull(skills);
var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
Assert.Equal(0, salvage.Xp);
}
[Fact]
public async Task PostInteract_ResourceNode_WhenSalvageDisallowsActivity_ShouldDenyWithoutGrants()
{
// Arrange
await using var factory = new SalvageActivityDeniedRegistryWebApplicationFactory();
var client = factory.CreateClient();
await MoveNearAlphaAsync(client);
// Act
var interact = await client.PostAsJsonAsync(
"/game/players/dev-local-1/interact",
new InteractionRequest
{
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
});
var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");
// Assert
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
var envelope = await interact.Content.ReadFromJsonAsync();
Assert.NotNull(envelope);
Assert.False(envelope!.Allowed);
Assert.Equal(SkillProgressionSnapshotApi.ReasonSourceKindNotAllowed, envelope.ReasonCode);
var bag = await inventory.Content.ReadFromJsonAsync();
Assert.NotNull(bag);
Assert.Equal(0, CountItem(bag!, "scrap_metal_bulk"));
var skills = await progression.Content.ReadFromJsonAsync();
Assert.NotNull(skills);
var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
Assert.Equal(0, salvage.Xp);
}
private static async Task MoveNearAlphaAsync(HttpClient client)
{
var move = new MoveCommandRequest
{
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector { X = 10, Y = 0.9, Z = -6 },
};
await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);
}
private static int CountItem(PlayerInventorySnapshotResponse snapshot, string itemId)
{
var total = 0;
foreach (var slot in snapshot.BagSlots!)
{
if (string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
{
total += slot.Quantity;
}
}
return total;
}
}