using System.Diagnostics.CodeAnalysis;
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.Gathering;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Npc;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Skills;
using Npgsql;
namespace NeonSprawl.Server.Tests.Game.Interaction;
///
/// Same in-memory overrides as , plus a stub
/// where salvage does not allow activity — for gather deny tests without mutating disk catalog.
///
public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebApplicationFactory
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
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.");
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("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(IPlayerInventoryStore) ||
d.ServiceType == typeof(IResourceNodeInstanceStore) ||
d.ServiceType == typeof(IPlayerPerkStateStore) ||
d.ServiceType == typeof(TimeProvider) ||
d.ServiceType == typeof(IPlayerAbilityCooldownStore) ||
d.ServiceType == typeof(ISkillDefinitionRegistry) ||
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));
services.AddSingleton(fakeTime);
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
});
}
}
/// Prototype trio ids; salvage excludes activity so gather interact grant path denies without throwing.
internal sealed class SalvageActivityDeniedSkillRegistry : ISkillDefinitionRegistry
{
private static readonly SkillDefRow SalvageNoActivity = new(
"salvage",
"gather",
"Salvage",
new[] { "mission_reward" });
private static readonly SkillDefRow Refine = new(
"refine",
"process",
"Refine",
new[] { "activity", "mission_reward", "trainer" });
private static readonly SkillDefRow Intrusion = new(
"intrusion",
"tech",
"Intrusion",
new[] { "activity", "mission_reward", "book_or_item" });
private static readonly IReadOnlyList Ordered =
[
Intrusion,
Refine,
SalvageNoActivity,
];
public bool TryGetDefinition(string? skillId, [NotNullWhen(true)] out SkillDefRow? definition)
{
if (skillId is null)
{
definition = null;
return false;
}
var k = skillId.Trim();
foreach (var row in Ordered)
{
if (string.Equals(row.Id, k, StringComparison.OrdinalIgnoreCase))
{
definition = row;
return true;
}
}
definition = null;
return false;
}
public IReadOnlyList GetDefinitionsInIdOrder() => Ordered;
}