using Microsoft.AspNetCore.Hosting; using NeonSprawl.Server.Game.Contracts; using NeonSprawl.Server.Game.Encounters; 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; namespace NeonSprawl.Server.Tests.Game.Quests; public sealed class QuestFixtureApiTests { private const string DevPlayer = "dev-local-1"; private const string FixturePath = $"/game/players/{DevPlayer}/__dev/quest-fixture"; private const string ChainQuestId = PrototypeE7M1QuestCatalogRules.ChainQuestId; private static QuestFixtureRequest ValidFixture(params string[] completedQuestIds) => new() { SchemaVersion = QuestFixtureRequest.CurrentSchemaVersion, CompletedQuestIds = completedQuestIds, }; [Fact] public async Task PostQuestFixture_ShouldReturnNotFound_WhenRouteNotRegistered() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.WithWebHostBuilder(b => { b.UseEnvironment("Production"); b.UseSetting("Game:EnableQuestFixtureApi", "false"); }).CreateClient(); // Act var response = await client.PostAsJsonAsync(FixturePath, ValidFixture(ChainQuestId)); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostQuestFixture_ShouldReturnBadRequest_WhenBodyNull() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( FixturePath, new StringContent(string.Empty, Encoding.UTF8, "application/json")); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostQuestFixture_ShouldReturnBadRequest_WhenSchemaVersionMismatch() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var request = new QuestFixtureRequest { SchemaVersion = QuestFixtureRequest.CurrentSchemaVersion + 99, CompletedQuestIds = [ChainQuestId], }; // Act var response = await client.PostAsJsonAsync(FixturePath, request); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostQuestFixture_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/missing-player/__dev/quest-fixture", ValidFixture(ChainQuestId)); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostQuestFixture_ShouldMarkOperatorChainCompleted_WhenNotStarted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var progressStore = factory.Services.GetRequiredService(); // Act var response = await client.PostAsJsonAsync(FixturePath, ValidFixture(ChainQuestId)); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.Applied); Assert.True(progressStore.TryGetProgress(DevPlayer, ChainQuestId, out var progress)); Assert.Equal(QuestProgressStatus.Completed, progress.Status); Assert.NotNull(progress.CompletedAt); } [Fact] public async Task PostQuestFixture_ShouldBeIdempotent_WhenQuestAlreadyCompleted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var first = await client.PostAsJsonAsync(FixturePath, ValidFixture(ChainQuestId)); Assert.Equal(HttpStatusCode.OK, first.StatusCode); // Act var second = await client.PostAsJsonAsync(FixturePath, ValidFixture(ChainQuestId)); // Assert Assert.Equal(HttpStatusCode.OK, second.StatusCode); } [Fact] public async Task PostQuestFixture_ShouldClearProgress_WhenResetQuestIdsProvided() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var progressStore = factory.Services.GetRequiredService(); var markCompleted = await client.PostAsJsonAsync(FixturePath, ValidFixture(ChainQuestId)); Assert.Equal(HttpStatusCode.OK, markCompleted.StatusCode); Assert.True(progressStore.TryGetProgress(DevPlayer, ChainQuestId, out _)); // Act var reset = await client.PostAsJsonAsync( FixturePath, new QuestFixtureRequest { SchemaVersion = QuestFixtureRequest.CurrentSchemaVersion, ResetQuestIds = [ChainQuestId], }); // Assert Assert.Equal(HttpStatusCode.OK, reset.StatusCode); Assert.False(progressStore.TryGetProgress(DevPlayer, ChainQuestId, out _)); } [Fact] public async Task PostQuestFixture_ResetAllPrototypeQuests_ShouldReturnNotStartedOnGetQuestProgress() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var markCompleted = await client.PostAsJsonAsync( FixturePath, ValidFixture(ChainQuestId, PrototypeE7M1QuestCatalogRules.GatherIntroQuestId)); Assert.Equal(HttpStatusCode.OK, markCompleted.StatusCode); // Act var reset = await client.PostAsJsonAsync( FixturePath, new QuestFixtureRequest { SchemaVersion = QuestFixtureRequest.CurrentSchemaVersion, ResetQuestIds = [ PrototypeE7M1QuestCatalogRules.CombatIntroQuestId, PrototypeE7M1QuestCatalogRules.GatherIntroQuestId, PrototypeE7M1QuestCatalogRules.GridContractQuestId, PrototypeE7M1QuestCatalogRules.ChainQuestId, PrototypeE7M1QuestCatalogRules.RefineIntroQuestId, ], }); var progress = await client.GetAsync($"/game/players/{DevPlayer}/quest-progress"); // Assert Assert.Equal(HttpStatusCode.OK, reset.StatusCode); Assert.Equal(HttpStatusCode.OK, progress.StatusCode); var body = await progress.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.All(body!.Quests, row => Assert.Equal(QuestProgressApi.StatusNotStarted, row.Status)); } [Fact] public async Task PostQuestFixture_ShouldClearFactionStanding_WhenResetFactionIdsProvided() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var standingStore = factory.Services.GetRequiredService(); const string gridFactionId = PrototypeE7M3QuestFactionRules.GridContractGateFactionId; var apply = ReputationOperations.TryApplyDelta( DevPlayer, gridFactionId, 15, "fixture-seed-standing", ReputationDeltaSourceKinds.QuestCompletion, ChainQuestId, standingStore, factory.Services.GetRequiredService(), factory.Services.GetRequiredService()); Assert.True(apply.Success); Assert.Equal(15, standingStore.TryGetStanding(DevPlayer, gridFactionId).Standing); // Act var reset = await client.PostAsJsonAsync( FixturePath, new QuestFixtureRequest { SchemaVersion = QuestFixtureRequest.CurrentSchemaVersion, ResetFactionIds = [gridFactionId], }); // Assert Assert.Equal(HttpStatusCode.OK, reset.StatusCode); Assert.Equal(0, standingStore.TryGetStanding(DevPlayer, gridFactionId).Standing); } [Fact] public async Task PostQuestFixture_ShouldClearRewardDeliveryAndAudit_WhenResetQuestIdsProvided() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var deliveryStore = factory.Services.GetRequiredService(); var auditStore = factory.Services.GetRequiredService(); const string gridFactionId = PrototypeE7M3QuestFactionRules.GridContractGateFactionId; var apply = ReputationOperations.TryApplyDelta( DevPlayer, gridFactionId, 15, RewardDeliveryIds.MakeReputationDeltaId( RewardDeliveryIds.MakeIdempotencyKey(DevPlayer, ChainQuestId), gridFactionId), ReputationDeltaSourceKinds.QuestCompletion, ChainQuestId, factory.Services.GetRequiredService(), auditStore, factory.Services.GetRequiredService()); Assert.True(apply.Success); Assert.True(deliveryStore.TryRecord(new RewardDeliveryEvent( DevPlayer, ChainQuestId, DateTimeOffset.UtcNow, [], [], [new RewardReputationGrantApplied(gridFactionId, 15)], RewardDeliveryIds.MakeIdempotencyKey(DevPlayer, ChainQuestId)))); // Act var reset = await client.PostAsJsonAsync( FixturePath, new QuestFixtureRequest { SchemaVersion = QuestFixtureRequest.CurrentSchemaVersion, ResetQuestIds = [ChainQuestId], }); // Assert Assert.Equal(HttpStatusCode.OK, reset.StatusCode); Assert.False(deliveryStore.TryGet(DevPlayer, ChainQuestId, out _)); Assert.Empty(auditStore.GetDeltasForPlayer(DevPlayer)); } [Fact] public async Task PostQuestFixture_ShouldClearContractStores_WhenResetContractInstanceIdsProvided() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var templateRegistry = factory.Services.GetRequiredService(); var instanceStore = factory.Services.GetRequiredService(); var outcomeStore = factory.Services.GetRequiredService(); var deliveryStore = factory.Services.GetRequiredService(); var standingStore = factory.Services.GetRequiredService(); var itemRegistry = factory.Services.GetRequiredService(); var inventoryStore = factory.Services.GetRequiredService(); var skillRegistry = factory.Services.GetRequiredService(); var skillProgressionStore = factory.Services.GetRequiredService(); var levelCurve = factory.Services.GetRequiredService(); var perkUnlockEngine = factory.Services.GetRequiredService(); var perkStore = factory.Services.GetRequiredService(); var auditStore = factory.Services.GetRequiredService(); var timeProvider = factory.Services.GetRequiredService(); const string templateId = "prototype_contract_clear_combat_pocket"; const string encounterId = "prototype_combat_pocket"; var encounterRegistry = factory.Services.GetRequiredService(); var factionRegistry = factory.Services.GetRequiredService(); var issued = ContractGeneratorOperations.TryIssue( DevPlayer, templateId, "2026-06-22", zoneDifficultyBand: null, templateRegistry, encounterRegistry, itemRegistry, skillRegistry, factionRegistry, instanceStore, standingStore, timeProvider); Assert.True(issued.Success); var instanceId = issued.Snapshot!.ContractInstanceId; var completed = ContractCompletionOperations.TryCompleteOnEncounterClear( DevPlayer, encounterId, instanceStore, outcomeStore, templateRegistry, itemRegistry, inventoryStore, skillRegistry, skillProgressionStore, levelCurve, perkUnlockEngine, perkStore, deliveryStore, standingStore, auditStore, timeProvider); Assert.True(completed.Success); Assert.True( deliveryStore.TryGet( DevPlayer, RewardDeliverySourceKinds.ContractCompletion, instanceId, out _)); Assert.Single(outcomeStore.GetOutcomesForInstance(instanceId)); // Act var reset = await client.PostAsJsonAsync( FixturePath, new QuestFixtureRequest { SchemaVersion = QuestFixtureRequest.CurrentSchemaVersion, ResetContractInstanceIds = [instanceId], }); // Assert Assert.Equal(HttpStatusCode.OK, reset.StatusCode); Assert.False(instanceStore.TryGet(DevPlayer, instanceId, out _)); Assert.False( deliveryStore.TryGet( DevPlayer, RewardDeliverySourceKinds.ContractCompletion, instanceId, out _)); Assert.Empty(outcomeStore.GetOutcomesForInstance(instanceId)); } }