using System.Linq; using System.Net; using System.Net.Http.Json; using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.Skills; using Xunit; namespace NeonSprawl.Server.Tests.Game.Skills; /// NEO-43: mission/quest reward hook → skill XP via shared NEO-38 grant path (sourceKind: mission_reward). public sealed class MissionRewardSkillXpGrantTests { private const int TestMissionRewardXp = 25; [Fact] public async Task GrantFromMissionReward_WhenCatalogAllowsMissionReward_ShouldIncreaseSalvageXp() { // Arrange — dev player is seeded on the in-memory progression store (NEO-38). await using var factory = new InMemoryWebApplicationFactory(); _ = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var registry = scope.ServiceProvider.GetRequiredService(); var xpStore = scope.ServiceProvider.GetRequiredService(); var levelCurve = scope.ServiceProvider.GetRequiredService(); // Act MissionRewardSkillXpGrant.GrantFromMissionReward( "dev-local-1", "salvage", TestMissionRewardXp, registry, xpStore, levelCurve); // Assert var totals = xpStore.GetXpTotals("dev-local-1"); Assert.True(totals.TryGetValue("salvage", out var xp)); Assert.Equal(TestMissionRewardXp, xp); } [Fact] public async Task GrantFromMissionReward_WhenCatalogAllowsMissionReward_ShouldMatchSkillProgressionGet() { // 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); using var scope = factory.Services.CreateScope(); var registry = scope.ServiceProvider.GetRequiredService(); var xpStore = scope.ServiceProvider.GetRequiredService(); var levelCurve = scope.ServiceProvider.GetRequiredService(); // Act MissionRewardSkillXpGrant.GrantFromMissionReward( "dev-local-1", "salvage", TestMissionRewardXp, registry, xpStore, levelCurve); var snapshot = await client.GetAsync("/game/players/dev-local-1/skill-progression"); // Assert Assert.Equal(HttpStatusCode.OK, snapshot.StatusCode); var body = await snapshot.Content.ReadFromJsonAsync(); Assert.NotNull(body); var salvage = body!.Skills!.Single(static s => s.Id == "salvage"); Assert.Equal(TestMissionRewardXp, salvage.Xp); } [Fact] public async Task GrantFromMissionReward_WhenSalvageDisallowsMissionReward_ShouldLeaveSalvageXpZero() { // Arrange await using var factory = new MissionRewardDeniedRegistryWebApplicationFactory(); _ = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var registry = scope.ServiceProvider.GetRequiredService(); var xpStore = scope.ServiceProvider.GetRequiredService(); var levelCurve = scope.ServiceProvider.GetRequiredService(); // Act MissionRewardSkillXpGrant.GrantFromMissionReward( "dev-local-1", "salvage", TestMissionRewardXp, registry, xpStore, levelCurve); // Assert — deny returns before TryApplyXpDelta; salvage row must not appear in stored XP map. var totals = xpStore.GetXpTotals("dev-local-1"); Assert.False(totals.ContainsKey("salvage")); } }