using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.Contracts; using NeonSprawl.Server.Game.Rewards; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.Rewards; public sealed class InMemoryRewardDeliveryStoreTests { private const string PlayerId = "dev-local-1"; private const string QuestId = "prototype_quest_operator_chain"; private static readonly DateTimeOffset DeliveredAt = new(2026, 6, 7, 12, 0, 0, TimeSpan.Zero); [Fact] public void TryRecord_ShouldBeIdempotent_AtStoreLayer() { // Arrange var store = new InMemoryRewardDeliveryStore(); var deliveryEvent = CreatePrototypeEvent(); // Act var first = store.TryRecord(deliveryEvent); var second = store.TryRecord(deliveryEvent with { DeliveredAt = DeliveredAt.AddHours(1) }); var third = store.TryRecord( deliveryEvent with { PlayerId = "DEV-LOCAL-1", QuestId = "PROTOTYPE_QUEST_OPERATOR_CHAIN", }); // Assert Assert.True(first); Assert.False(second); Assert.False(third); Assert.True(store.TryGet(PlayerId, QuestId, out var stored)); Assert.Equal(deliveryEvent.IdempotencyKey, stored.IdempotencyKey); Assert.Equal(DeliveredAt, stored.DeliveredAt); Assert.Single(stored.GrantedItems); Assert.Single(stored.GrantedSkillXp); } [Fact] public void TryRecord_ShouldFailClosed_WhenPlayerIdIsEmpty() { // Arrange var store = new InMemoryRewardDeliveryStore(); var invalidEvent = CreatePrototypeEvent() with { PlayerId = " " }; // Act var recorded = store.TryRecord(invalidEvent); // Assert Assert.False(recorded); Assert.False(store.TryGet(" ", QuestId, out _)); } [Fact] public void TryRecord_ShouldFailClosed_WhenQuestIdIsEmpty() { // Arrange var store = new InMemoryRewardDeliveryStore(); var invalidEvent = CreatePrototypeEvent() with { QuestId = " " }; // Act var recorded = store.TryRecord(invalidEvent); // Assert Assert.False(recorded); Assert.False(store.TryGet(PlayerId, " ", out _)); } [Fact] public void TryGet_ShouldExposeReadableGrantSummary() { // Arrange var store = new InMemoryRewardDeliveryStore(); var deliveryEvent = CreatePrototypeEvent(); // Act var recorded = store.TryRecord(deliveryEvent); var found = store.TryGet(PlayerId, QuestId, out var stored); // Assert Assert.True(recorded); Assert.True(found); Assert.Equal("survey_drone_kit", stored.GrantedItems[0].ItemId); Assert.Equal(1, stored.GrantedItems[0].Quantity); Assert.Equal("salvage", stored.GrantedSkillXp[0].SkillId); Assert.Equal(50, stored.GrantedSkillXp[0].Amount); } [Fact] public async Task Host_ShouldResolveRewardDeliveryStoreFromDi_WhenStartupSucceeds() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); using var client = factory.CreateClient(); _ = await client.GetAsync("/health"); var deliveryStore = factory.Services.GetRequiredService(); // Act var recorded = deliveryStore.TryRecord(CreatePrototypeEvent()); var found = deliveryStore.TryGet(PlayerId, QuestId, out var stored); // Assert Assert.IsType(deliveryStore); Assert.True(recorded); Assert.True(found); Assert.Equal(RewardDeliveryIds.MakeIdempotencyKey(PlayerId, QuestId), stored.IdempotencyKey); } [Fact] public void TryClear_ShouldRemoveDeliveryRow_WhenPresent() { // Arrange var store = new InMemoryRewardDeliveryStore(); var deliveryEvent = CreatePrototypeEvent(); Assert.True(store.TryRecord(deliveryEvent)); // Act var cleared = store.TryClear(PlayerId, QuestId); // Assert Assert.True(cleared); Assert.False(store.TryGet(PlayerId, QuestId, out _)); } [Fact] public void TryClear_ShouldReturnTrue_WhenRowAlreadyAbsent() { // Arrange var store = new InMemoryRewardDeliveryStore(); // Act var cleared = store.TryClear(PlayerId, QuestId); // Assert Assert.True(cleared); } [Fact] public void TryRecord_ShouldIsolateContractRows_FromQuestRowsWithSameSourceId() { // Arrange var store = new InMemoryRewardDeliveryStore(); const string sharedSourceId = "prototype_shared_source_id"; var questEvent = new RewardDeliveryEvent( PlayerId, sharedSourceId, DeliveredAt, [new RewardItemGrantApplied("survey_drone_kit", 1)], [], [], RewardDeliveryIds.MakeIdempotencyKey(PlayerId, sharedSourceId)); var contractEvent = new RewardDeliveryEvent( PlayerId, QuestId: string.Empty, DeliveredAt, [new RewardItemGrantApplied("scrap_metal_bulk", 5)], [], [], ContractOutcomeIds.MakeIdempotencyKey(PlayerId, sharedSourceId), RewardDeliverySourceKinds.ContractCompletion, sharedSourceId); // Act var questRecorded = store.TryRecord(questEvent); var contractRecorded = store.TryRecord(contractEvent); // Assert Assert.True(questRecorded); Assert.True(contractRecorded); Assert.True(store.TryGet(PlayerId, sharedSourceId, out var storedQuest)); Assert.True( store.TryGet( PlayerId, RewardDeliverySourceKinds.ContractCompletion, sharedSourceId, out var storedContract)); Assert.Equal(RewardDeliverySourceKinds.QuestCompletion, storedQuest.SourceKind); Assert.Equal(RewardDeliverySourceKinds.ContractCompletion, storedContract.SourceKind); Assert.Equal("survey_drone_kit", storedQuest.GrantedItems[0].ItemId); Assert.Equal("scrap_metal_bulk", storedContract.GrantedItems[0].ItemId); } private static RewardDeliveryEvent CreatePrototypeEvent() => new( PlayerId, QuestId, DeliveredAt, [new RewardItemGrantApplied("survey_drone_kit", 1)], [new RewardSkillXpGrantApplied("salvage", 50)], [], RewardDeliveryIds.MakeIdempotencyKey(PlayerId, QuestId)); }