using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Time.Testing; using NeonSprawl.Server.Game.AbilityInput; using NeonSprawl.Server.Game.Combat; using NeonSprawl.Server.Game.Crafting; using NeonSprawl.Server.Game.Encounters; using NeonSprawl.Server.Game.Factions; using NeonSprawl.Server.Game.Gathering; using NeonSprawl.Server.Game.Gigs; using NeonSprawl.Server.Game.Items; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.Quests; using NeonSprawl.Server.Game.Mastery; using NeonSprawl.Server.Game.Npc; using NeonSprawl.Server.Game.Skills; using Npgsql; namespace NeonSprawl.Server.Tests; /// Forces the in-memory position store by replacing Postgres registrations after the host builds services (overrides process env e.g. CI ConnectionStrings__NeonSprawl). public sealed class InMemoryWebApplicationFactory : WebApplicationFactory { /// Controllable UTC clock shared by cast + cooldown routes in tests (NEO-32). public FakeTimeProvider? FakeClock { get; private set; } protected override void ConfigureWebHost(IWebHostBuilder builder) { // Avoid Development + DeveloperExceptionPage: invalid JSON / binding failures must stay 400 for API tests, // and Release CI runs rely on deterministic status codes (e.g. HotbarLoadoutApiTests malformed-body case). builder.UseEnvironment("Testing"); var skillsDir = SkillCatalogPathResolution.TryDiscoverSkillsDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/skills from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var masteryDir = MasteryCatalogPathResolution.TryDiscoverMasteryDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/mastery from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var itemsDir = ItemCatalogPathResolution.TryDiscoverItemsDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/items from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var resourceNodesDir = ResourceNodeCatalogPathResolution.TryDiscoverResourceNodesDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/resource-nodes from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var recipesDir = RecipeCatalogPathResolution.TryDiscoverRecipesDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/recipes from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var abilitiesDir = AbilityCatalogPathResolution.TryDiscoverAbilitiesDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/abilities from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var npcBehaviorsDir = NpcBehaviorCatalogPathResolution.TryDiscoverNpcBehaviorsDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/npc-behaviors from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var rewardTablesDir = RewardTableCatalogPathResolution.TryDiscoverRewardTablesDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/reward-tables from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var encountersDir = EncounterCatalogPathResolution.TryDiscoverEncountersDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/encounters from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var questsDir = QuestCatalogPathResolution.TryDiscoverQuestsDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/quests from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var factionsDir = FactionCatalogPathResolution.TryDiscoverFactionsDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/factions from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); builder.UseSetting("Content:SkillsDirectory", skillsDir); builder.UseSetting("Content:MasteryDirectory", masteryDir); builder.UseSetting("Content:ItemsDirectory", itemsDir); builder.UseSetting("Content:ResourceNodesDirectory", resourceNodesDir); builder.UseSetting("Content:RecipesDirectory", recipesDir); builder.UseSetting("Content:AbilitiesDirectory", abilitiesDir); builder.UseSetting("Content:NpcBehaviorsDirectory", npcBehaviorsDir); builder.UseSetting("Content:RewardTablesDirectory", rewardTablesDir); builder.UseSetting("Content:EncountersDirectory", encountersDir); builder.UseSetting("Content:FactionsDirectory", factionsDir); builder.UseSetting("Content:QuestsDirectory", questsDir); builder.UseSetting("Game:EnableMasteryFixtureApi", "true"); builder.ConfigureTestServices(services => { for (var i = services.Count - 1; i >= 0; i--) { var d = services[i]; if (d.ServiceType == typeof(IPositionStateStore) || d.ServiceType == typeof(IPlayerHotbarLoadoutStore) || d.ServiceType == typeof(IPlayerSkillProgressionStore) || d.ServiceType == typeof(IPlayerGigProgressionStore) || d.ServiceType == typeof(IPlayerQuestStateStore) || d.ServiceType == typeof(IFactionStandingStore) || d.ServiceType == typeof(IReputationDeltaStore) || d.ServiceType == typeof(IPlayerInventoryStore) || d.ServiceType == typeof(IResourceNodeInstanceStore) || d.ServiceType == typeof(IPlayerPerkStateStore) || d.ServiceType == typeof(TimeProvider) || d.ServiceType == typeof(IPlayerAbilityCooldownStore) || d.ServiceType == typeof(NpgsqlDataSource) || (d.ServiceType == typeof(IHostedService) && (d.ImplementationType == typeof(PostgresDevPlayerSeedHostedService) || d.ImplementationType == typeof(NpgsqlDataSourceShutdownHostedService)))) { services.RemoveAt(i); } } var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 4, 30, 12, 0, 0, TimeSpan.Zero)); FakeClock = fakeTime; services.AddSingleton(fakeTime); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); }); } }