using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.Factions; using NeonSprawl.Server.Game.Items; using NeonSprawl.Server.Game.Mastery; using NeonSprawl.Server.Game.Quests; using NeonSprawl.Server.Game.Rewards; using NeonSprawl.Server.Game.Skills; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.Quests; public sealed class QuestAcceptApiTests { private const string PlayerId = "dev-local-1"; private const string GatherQuestId = PrototypeE7M1QuestCatalogRules.GatherIntroQuestId; private const string RefineQuestId = PrototypeE7M1QuestCatalogRules.RefineIntroQuestId; private const string ChainQuestId = PrototypeE7M1QuestCatalogRules.ChainQuestId; private const string GridContractQuestId = PrototypeE7M1QuestCatalogRules.GridContractQuestId; [Fact] public async Task PostQuestAccept_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( $"/game/players/missing-player/quests/{GatherQuestId}/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostQuestAccept_ShouldReturnNotFound_WhenPlayerIdIsWhitespaceOnly() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( $"/game/players/%20/quests/{GatherQuestId}/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostQuestAccept_ShouldReturnAcceptedTrue_WhenGatherIntroNotStarted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/{GatherQuestId}/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal(QuestAcceptResponse.CurrentSchemaVersion, body!.SchemaVersion); Assert.True(body.Accepted); Assert.Null(body.ReasonCode); Assert.NotNull(body.Quest); Assert.Equal(GatherQuestId, body.Quest!.QuestId); Assert.Equal(QuestProgressApi.StatusActive, body.Quest.Status); Assert.Equal(0, body.Quest.CurrentStepIndex); Assert.Empty(body.Quest.ObjectiveCounters); Assert.Null(body.Quest.CompletedAt); Assert.Null(body.Quest.CompletionRewardSummary); var getResponse = await client.GetAsync($"/game/players/{PlayerId}/quest-progress"); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); var progress = await getResponse.Content.ReadFromJsonAsync(); Assert.NotNull(progress); var row = Assert.Single(progress!.Quests, static r => r.QuestId == GatherQuestId); Assert.Equal(QuestProgressApi.StatusActive, row.Status); } [Fact] public async Task PostQuestAccept_ShouldReturnAcceptedTrue_WhenRequestBodyOmitted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/{GatherQuestId}/accept", content: null); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.Accepted); Assert.Equal(GatherQuestId, body.Quest!.QuestId); } [Fact] public async Task PostQuestAccept_ShouldReturnAcceptedTrue_WhenRequestBodyIsEmptyObject() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); using var content = new StringContent("{}", Encoding.UTF8, "application/json"); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/{GatherQuestId}/accept", content); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.Accepted); Assert.Equal(GatherQuestId, body.Quest!.QuestId); } [Fact] public async Task PostQuestAccept_ShouldReturnBadRequest_WhenSchemaVersionInvalid() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); using var content = new StringContent( JsonSerializer.Serialize(new { schemaVersion = 99 }), Encoding.UTF8, "application/json"); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/{GatherQuestId}/accept", content); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostQuestAccept_ShouldDenyPrerequisiteIncomplete_WhenRefineIntroBeforeGatherComplete() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/{RefineQuestId}/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(QuestStateReasonCodes.PrerequisiteIncomplete, body.ReasonCode); Assert.Null(body.Quest); var getResponse = await client.GetAsync($"/game/players/{PlayerId}/quest-progress"); var progress = await getResponse.Content.ReadFromJsonAsync(); Assert.NotNull(progress); var row = Assert.Single(progress!.Quests, static r => r.QuestId == RefineQuestId); Assert.Equal(QuestProgressApi.StatusNotStarted, row.Status); } [Fact] public async Task PostQuestAccept_ShouldDenyAlreadyActive_WhenQuestAlreadyAccepted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); _ = await client.PostAsync( $"/game/players/{PlayerId}/quests/{GatherQuestId}/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/{GatherQuestId}/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(QuestStateReasonCodes.AlreadyActive, body.ReasonCode); Assert.NotNull(body.Quest); Assert.Equal(QuestProgressApi.StatusActive, body.Quest!.Status); Assert.Equal(0, body.Quest.CurrentStepIndex); } [Fact] public async Task PostQuestAccept_ShouldDenyAlreadyCompleted_WhenQuestAlreadyCompleted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var services = factory.Services; var questRegistry = services.GetRequiredService(); var progressStore = services.GetRequiredService(); var inventoryStore = services.GetRequiredService(); var itemRegistry = services.GetRequiredService(); var skillRegistry = services.GetRequiredService(); var skillProgressionStore = services.GetRequiredService(); var levelCurve = services.GetRequiredService(); var perkUnlockEngine = services.GetRequiredService(); var perkStore = services.GetRequiredService(); var deliveryStore = services.GetRequiredService(); var standingStore = services.GetRequiredService(); var timeProvider = TimeProvider.System; Assert.True(QuestStateOperations.TryAccept( PlayerId, GatherQuestId, questRegistry, progressStore, inventoryStore, itemRegistry, skillRegistry, skillProgressionStore, levelCurve, perkUnlockEngine, perkStore, deliveryStore, standingStore, timeProvider).Success); Assert.True(QuestStateOperations.TryMarkComplete( PlayerId, GatherQuestId, questRegistry, progressStore, itemRegistry, inventoryStore, skillRegistry, skillProgressionStore, levelCurve, perkUnlockEngine, perkStore, deliveryStore, timeProvider).Success); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/{GatherQuestId}/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(QuestStateReasonCodes.AlreadyCompleted, body.ReasonCode); Assert.NotNull(body.Quest); Assert.Equal(QuestProgressApi.StatusCompleted, body.Quest!.Status); Assert.Equal(0, body.Quest.CurrentStepIndex); Assert.NotNull(body.Quest.CompletedAt); Assert.NotNull(body.Quest.CompletionRewardSummary); Assert.Empty(body.Quest.CompletionRewardSummary!.ItemGrants); Assert.Single(body.Quest.CompletionRewardSummary.SkillXpGrants); Assert.Equal("salvage", body.Quest.CompletionRewardSummary.SkillXpGrants[0].SkillId); Assert.Equal(25, body.Quest.CompletionRewardSummary.SkillXpGrants[0].Amount); } [Fact] public async Task PostQuestAccept_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/prototype_quest_missing/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(QuestStateReasonCodes.UnknownQuest, body.ReasonCode); Assert.Null(body.Quest); } [Fact] public async Task PostQuestAccept_ShouldDenyFactionGateBlocked_WhenGridContractBelowThreshold() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var progressStore = factory.Services.GetRequiredService(); Assert.True(progressStore.TryActivate(PlayerId, ChainQuestId, out _)); Assert.True(progressStore.TryMarkComplete(PlayerId, ChainQuestId, DateTimeOffset.UtcNow, out _)); // Act var response = await client.PostAsync( $"/game/players/{PlayerId}/quests/{GridContractQuestId}/accept", JsonContent.Create(new QuestAcceptRequest { SchemaVersion = QuestAcceptRequest.CurrentSchemaVersion })); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(QuestStateReasonCodes.FactionGateBlocked, body.ReasonCode); Assert.Null(body.Quest); } }