using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.Quests; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.Quests; public sealed class QuestStateOperationsTests { private const string PlayerId = "dev-local-1"; private const string UnknownPlayerId = "unknown-player-xyz"; private const string GatherQuestId = PrototypeE7M1QuestCatalogRules.GatherIntroQuestId; private const string RefineQuestId = PrototypeE7M1QuestCatalogRules.RefineIntroQuestId; private const string CombatQuestId = PrototypeE7M1QuestCatalogRules.CombatIntroQuestId; private const string ChainQuestId = PrototypeE7M1QuestCatalogRules.ChainQuestId; private const string ChainObjectiveId = PrototypeE7M1QuestCatalogRules.ChainFirstObjectiveId; [Fact] public async Task TryAccept_ShouldActivateGatherIntro_WhenNotStarted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); // Act var result = QuestStateOperations.TryAccept( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.True(result.Success); Assert.Null(result.ReasonCode); Assert.NotNull(result.Snapshot); Assert.Equal(QuestProgressStatus.Active, result.Snapshot!.Status); Assert.Equal(0, result.Snapshot.CurrentStepIndex); } [Fact] public async Task TryAccept_ShouldDenyPrerequisiteIncomplete_WhenRefineIntroBeforeGatherComplete() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); // Act var result = QuestStateOperations.TryAccept( PlayerId, RefineQuestId, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.PrerequisiteIncomplete, result.ReasonCode); Assert.Null(result.Snapshot); } [Fact] public async Task TryAccept_ShouldSucceedRefineIntro_WhenGatherIntroCompleted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); var timeProvider = TimeProvider.System; Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success); Assert.True(QuestStateOperations.TryMarkComplete( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore, timeProvider).Success); // Act var result = QuestStateOperations.TryAccept( PlayerId, RefineQuestId, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.True(result.Success); Assert.Null(result.ReasonCode); Assert.Equal(QuestProgressStatus.Active, result.Snapshot!.Status); } [Fact] public async Task TryAccept_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); // Act var result = QuestStateOperations.TryAccept( PlayerId, "prototype_quest_missing", deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.UnknownQuest, result.ReasonCode); } [Fact] public async Task TryAccept_ShouldDenyAlreadyActive_WhenQuestAlreadyAccepted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success); // Act var result = QuestStateOperations.TryAccept( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.AlreadyActive, result.ReasonCode); Assert.NotNull(result.Snapshot); Assert.Equal(QuestProgressStatus.Active, result.Snapshot!.Status); } [Fact] public async Task TryAccept_ShouldDenyAlreadyCompleted_WhenQuestAlreadyCompleted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); var timeProvider = TimeProvider.System; Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success); Assert.True(QuestStateOperations.TryMarkComplete( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore, timeProvider).Success); // Act var result = QuestStateOperations.TryAccept( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.AlreadyCompleted, result.ReasonCode); Assert.Equal(QuestProgressStatus.Completed, result.Snapshot!.Status); } [Fact] public async Task TryAccept_ShouldDenyUnknownPlayer_WhenPlayerNotInStore() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); // Act var result = QuestStateOperations.TryAccept( UnknownPlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.UnknownPlayer, result.ReasonCode); } [Fact] public async Task TryAdvanceStep_ShouldAdvanceChainQuestAndClearCounters() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); var timeProvider = TimeProvider.System; AcceptAndComplete(deps, GatherQuestId, timeProvider); AcceptAndComplete(deps, RefineQuestId, timeProvider); AcceptAndComplete(deps, CombatQuestId, timeProvider); Assert.True(QuestStateOperations.TryAccept(PlayerId, ChainQuestId, deps.QuestRegistry, deps.ProgressStore).Success); Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, ChainObjectiveId, 5, out _)); // Act var result = QuestStateOperations.TryAdvanceStep( PlayerId, ChainQuestId, 1, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.True(result.Success); Assert.Null(result.ReasonCode); Assert.Equal(1, result.Snapshot!.CurrentStepIndex); Assert.Empty(result.Snapshot.ObjectiveCounters); } [Theory] [InlineData(0)] [InlineData(4)] public async Task TryAdvanceStep_ShouldDenyInvalidStepIndex_WhenIndexNotIncreasingOrPastLastStep(int newStepIndex) { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); var timeProvider = TimeProvider.System; AcceptAndComplete(deps, GatherQuestId, timeProvider); AcceptAndComplete(deps, RefineQuestId, timeProvider); AcceptAndComplete(deps, CombatQuestId, timeProvider); Assert.True(QuestStateOperations.TryAccept(PlayerId, ChainQuestId, deps.QuestRegistry, deps.ProgressStore).Success); // Act var result = QuestStateOperations.TryAdvanceStep( PlayerId, ChainQuestId, newStepIndex, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.InvalidStepIndex, result.ReasonCode); } [Fact] public async Task TryAdvanceStep_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); // Act var result = QuestStateOperations.TryAdvanceStep( PlayerId, "prototype_quest_missing", 1, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.UnknownQuest, result.ReasonCode); } [Fact] public async Task TryMarkComplete_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); // Act var result = QuestStateOperations.TryMarkComplete( PlayerId, "prototype_quest_missing", deps.QuestRegistry, deps.ProgressStore, TimeProvider.System); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.UnknownQuest, result.ReasonCode); } [Fact] public async Task TryAdvanceStep_ShouldDenyNotActive_WhenQuestNotAccepted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); // Act var result = QuestStateOperations.TryAdvanceStep( PlayerId, GatherQuestId, 1, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.NotActive, result.ReasonCode); } [Fact] public async Task TryAdvanceStep_ShouldDenyAlreadyCompleted_WhenQuestCompleted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); var timeProvider = TimeProvider.System; Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success); Assert.True(QuestStateOperations.TryMarkComplete( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore, timeProvider).Success); // Act var result = QuestStateOperations.TryAdvanceStep( PlayerId, GatherQuestId, 1, deps.QuestRegistry, deps.ProgressStore); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.AlreadyCompleted, result.ReasonCode); } [Fact] public async Task TryMarkComplete_ShouldBeIdempotentAtOperationsLayer() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); var timeProvider = TimeProvider.System; Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success); // Act var first = QuestStateOperations.TryMarkComplete( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore, timeProvider); var second = QuestStateOperations.TryMarkComplete( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore, timeProvider); // Assert Assert.True(first.Success); Assert.Null(first.ReasonCode); Assert.NotNull(first.Snapshot!.CompletedAt); Assert.True(second.Success); Assert.Null(second.ReasonCode); Assert.Equal(first.Snapshot.CompletedAt, second.Snapshot!.CompletedAt); Assert.Equal(QuestProgressStatus.Completed, second.Snapshot.Status); } [Fact] public async Task TryMarkComplete_ShouldDenyNotActive_WhenQuestNeverAccepted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); var timeProvider = TimeProvider.System; // Act var result = QuestStateOperations.TryMarkComplete( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore, timeProvider); // Assert Assert.False(result.Success); Assert.Equal(QuestStateReasonCodes.NotActive, result.ReasonCode); } [Fact] public async Task Host_ShouldResolveRegistryAndStore_ForQuestStateOperations() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveDependencies(factory); // Act var accept = QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore); var complete = QuestStateOperations.TryMarkComplete( PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore, TimeProvider.System); // Assert Assert.True(accept.Success); Assert.True(complete.Success); Assert.Equal(QuestProgressStatus.Completed, complete.Snapshot!.Status); } private static void AcceptAndComplete( (IQuestDefinitionRegistry QuestRegistry, IPlayerQuestStateStore ProgressStore) deps, string questId, TimeProvider timeProvider) { Assert.True(QuestStateOperations.TryAccept(PlayerId, questId, deps.QuestRegistry, deps.ProgressStore).Success); Assert.True(QuestStateOperations.TryMarkComplete( PlayerId, questId, deps.QuestRegistry, deps.ProgressStore, timeProvider).Success); } private static (IQuestDefinitionRegistry QuestRegistry, IPlayerQuestStateStore ProgressStore) ResolveDependencies( InMemoryWebApplicationFactory factory) { var services = factory.Services; return ( services.GetRequiredService(), services.GetRequiredService()); } }