using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.Quests; namespace NeonSprawl.Server.Tests.Game.Quests; public sealed class InMemoryPlayerQuestStateStoreTests { private const string PlayerId = "dev-local-1"; private const string UnknownPlayerId = "unknown-player-xyz"; private const string GatherQuestId = PrototypeE7M1QuestCatalogRules.GatherIntroQuestId; private const string ChainQuestId = PrototypeE7M1QuestCatalogRules.ChainQuestId; private const string GatherObjectiveId = PrototypeE7M1QuestCatalogRules.GatherIntroFirstObjectiveId; private static readonly DateTimeOffset CompletedAt = new(2026, 6, 3, 12, 0, 0, TimeSpan.Zero); [Fact] public void TryGetProgress_ShouldReturnFalse_WhenRowMissing() { // Arrange var store = CreateStore(); // Act var found = store.TryGetProgress(PlayerId, GatherQuestId, out _); // Assert Assert.False(found); } [Fact] public void TryActivate_ShouldCreateActiveRowAtStepZero() { // Arrange var store = CreateStore(); // Act var activated = store.TryActivate(PlayerId, GatherQuestId, out var snapshot); // Assert Assert.True(activated); Assert.Equal(QuestProgressStatus.Active, snapshot.Status); Assert.Equal(0, snapshot.CurrentStepIndex); Assert.Empty(snapshot.ObjectiveCounters); Assert.Null(snapshot.CompletedAt); } [Fact] public void TryActivate_ShouldReturnFalse_WhenAlreadyActive() { // Arrange var store = CreateStore(); Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _)); // Act var second = store.TryActivate(PlayerId, GatherQuestId, out var snapshot); // Assert Assert.False(second); Assert.Equal(QuestProgressStatus.Active, snapshot.Status); } [Fact] public void TryUpdateObjectiveCounter_ShouldUpdateMap_WhenActive() { // Arrange var store = CreateStore(); Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _)); // Act var updated = store.TryUpdateObjectiveCounter(PlayerId, GatherQuestId, GatherObjectiveId, 3, out var snapshot); // Assert Assert.True(updated); Assert.Equal(3, snapshot.ObjectiveCounters[GatherObjectiveId]); } [Fact] public void TryAdvanceStep_ShouldClearCountersAndBumpIndex() { // Arrange var store = CreateStore(); Assert.True(store.TryActivate(PlayerId, ChainQuestId, out _)); Assert.True(store.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, PrototypeE7M1QuestCatalogRules.ChainFirstObjectiveId, 5, out _)); // Act var advanced = store.TryAdvanceStep(PlayerId, ChainQuestId, 1, out var snapshot); // Assert Assert.True(advanced); Assert.Equal(1, snapshot.CurrentStepIndex); Assert.Empty(snapshot.ObjectiveCounters); } [Fact] public void TryAdvanceStep_ShouldReturnFalse_WhenIndexNotIncreasing() { // Arrange var store = CreateStore(); Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _)); // Act var sameIndex = store.TryAdvanceStep(PlayerId, GatherQuestId, 0, out _); // Assert Assert.False(sameIndex); } [Fact] public void TryMarkComplete_ShouldBeIdempotent() { // Arrange var store = CreateStore(); Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _)); // Act var first = store.TryMarkComplete(PlayerId, GatherQuestId, CompletedAt, out var firstSnapshot); var second = store.TryMarkComplete(PlayerId, GatherQuestId, CompletedAt.AddHours(1), out var secondSnapshot); // Assert Assert.True(first); Assert.False(second); Assert.Equal(QuestProgressStatus.Completed, secondSnapshot.Status); Assert.Equal(CompletedAt, secondSnapshot.CompletedAt); Assert.Equal(firstSnapshot.CurrentStepIndex, secondSnapshot.CurrentStepIndex); } [Fact] public void TryMarkComplete_ShouldDenyRegression_OnActivateAdvanceAndCounter() { // Arrange var store = CreateStore(); Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _)); Assert.True(store.TryMarkComplete(PlayerId, GatherQuestId, CompletedAt, out _)); // Act var reactivate = store.TryActivate(PlayerId, GatherQuestId, out _); var advance = store.TryAdvanceStep(PlayerId, GatherQuestId, 1, out _); var counter = store.TryUpdateObjectiveCounter(PlayerId, GatherQuestId, GatherObjectiveId, 1, out _); // Assert Assert.False(reactivate); Assert.False(advance); Assert.False(counter); Assert.True(store.TryGetProgress(PlayerId, GatherQuestId, out var snapshot)); Assert.Equal(QuestProgressStatus.Completed, snapshot.Status); } [Fact] public void TryActivate_ShouldReturnFalse_ForUnknownPlayer() { // Arrange var store = CreateStore(); // Act var activated = store.TryActivate(UnknownPlayerId, GatherQuestId, out _); // Assert Assert.False(activated); } [Theory] [InlineData("", GatherQuestId)] [InlineData(" ", GatherQuestId)] [InlineData(PlayerId, "")] [InlineData(PlayerId, " ")] public void TryGetProgressAndActivate_ShouldReturnFalse_WhenPlayerOrQuestIdEmpty( string playerId, string questId) { // Arrange var store = CreateStore(); // Act var getProgress = store.TryGetProgress(playerId, questId, out _); var activate = store.TryActivate(playerId, questId, out _); // Assert Assert.False(getProgress); Assert.False(activate); } [Fact] public async Task Host_ShouldResolveStore_AndActivateGatherIntro() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); using var scope = factory.Services.CreateScope(); var store = scope.ServiceProvider.GetRequiredService(); // Act var activated = store.TryActivate(PlayerId, GatherQuestId, out var snapshot); var found = store.TryGetProgress(PlayerId, GatherQuestId, out var readBack); // Assert Assert.True(activated); Assert.True(found); Assert.Equal(snapshot.QuestId, readBack.QuestId); Assert.Equal(QuestProgressStatus.Active, readBack.Status); } private static InMemoryPlayerQuestStateStore CreateStore() { var options = Microsoft.Extensions.Options.Options.Create(new GamePositionOptions { DevPlayerId = PlayerId }); return new InMemoryPlayerQuestStateStore(options); } }