using System.IO; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using NeonSprawl.Server.Game.AbilityInput; using NeonSprawl.Server.Game.Combat; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.Combat; public class AbilityDefinitionRegistryTests { private static AbilityDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary byId) { var catalog = new AbilityDefinitionCatalog("/tmp/abilities", byId, catalogJsonFileCount: 1); return new AbilityDefinitionRegistry(catalog); } [Fact] public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenPulseExists() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypePulse, "Prototype Pulse", 25, 3.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var def); // Assert Assert.True(found); Assert.NotNull(def); Assert.Equal(25, def.BaseDamage); Assert.Equal(3.0, def.CooldownSeconds); Assert.Equal("attack", def.AbilityKind); Assert.Equal("Prototype Pulse", def.DisplayName); } [Fact] public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenBurstExists() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypeBurst] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypeBurst, "Prototype Burst", 40, 5.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeBurst, out var def); // Assert Assert.True(found); Assert.NotNull(def); Assert.Equal(40, def.BaseDamage); Assert.Equal(5.0, def.CooldownSeconds); } [Fact] public void TryGetDefinition_ShouldReturnFalse_WhenAbilityIdIsNull() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypePulse, "Prototype Pulse", 25, 3.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition(null, out var def); // Assert Assert.False(found); Assert.Null(def); } [Fact] public void TryGetDefinition_ShouldReturnFalse_WhenIdUnknown() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypePulse, "Prototype Pulse", 25, 3.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition("not_a_real_ability", out var def); // Assert Assert.False(found); Assert.Null(def); } [Fact] public void TryNormalizeKnown_ShouldReturnTrueAndLowercaseId_WhenInputHasWhitespaceAndMixedCase() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypePulse, "Prototype Pulse", 25, 3.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryNormalizeKnown(" Prototype_Pulse ", out var normalized); // Assert Assert.True(found); Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, normalized); } [Fact] public void TryNormalizeKnown_ShouldReturnFalse_WhenInputIsNull() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypePulse, "Prototype Pulse", 25, 3.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryNormalizeKnown(null, out var normalized); // Assert Assert.False(found); Assert.Equal(string.Empty, normalized); } [Fact] public void TryNormalizeKnown_ShouldReturnFalse_WhenInputIsWhitespaceOnly() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypePulse, "Prototype Pulse", 25, 3.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryNormalizeKnown(" ", out var normalized); // Assert Assert.False(found); Assert.Equal(string.Empty, normalized); } [Fact] public void TryNormalizeKnown_ShouldReturnFalse_WhenIdNotInCatalog() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypePulse, "Prototype Pulse", 25, 3.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryNormalizeKnown("prototype_unknown", out var normalized); // Assert Assert.False(found); Assert.Equal("prototype_unknown", normalized); } [Fact] public void GetDefinitionsInIdOrder_ShouldListAllRowsOrderedById_WhenMultipleAbilities() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeAbilityRegistry.PrototypeBurst] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypeBurst, "Prototype Burst", 40, 5.0, 6.0, "attack"), [PrototypeAbilityRegistry.PrototypeDash] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypeDash, "Prototype Dash", 0, 4.0, 6.0, "movement"), [PrototypeAbilityRegistry.PrototypeGuard] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypeGuard, "Prototype Guard", 0, 6.0, 6.0, "utility"), [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow( PrototypeAbilityRegistry.PrototypePulse, "Prototype Pulse", 25, 3.0, 6.0, "attack"), }; var registry = CreateRegistryFromRows(rows); // Act var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.Equal(4, list.Count); Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, list[0].Id); Assert.Equal(PrototypeAbilityRegistry.PrototypeDash, list[1].Id); Assert.Equal(PrototypeAbilityRegistry.PrototypeGuard, list[2].Id); Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, list[3].Id); } [Fact] public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture() { // Arrange var root = Directory.CreateTempSubdirectory("neon-sprawl-ability-registry-loader-"); try { var abilitiesDir = Path.Combine(root.FullName, "content", "abilities"); var schemaDir = Path.Combine(root.FullName, "content", "schemas"); Directory.CreateDirectory(abilitiesDir); Directory.CreateDirectory(schemaDir); var schemaPath = Path.Combine(schemaDir, "ability-def.schema.json"); File.Copy(AbilityCatalogTestPaths.DiscoverRepoAbilityDefSchemaPath(), schemaPath, overwrite: true); File.Copy( Path.Combine(AbilityCatalogTestPaths.DiscoverRepoAbilitiesDirectory(), "prototype_abilities.json"), Path.Combine(abilitiesDir, "prototype_abilities.json"), overwrite: true); File.Copy( Path.Combine(AbilityCatalogTestPaths.DiscoverRepoAbilitiesDirectory(), "prototype_npc_abilities.json"), Path.Combine(abilitiesDir, "prototype_npc_abilities.json"), overwrite: true); var loaded = AbilityDefinitionCatalogLoader.Load(abilitiesDir, schemaPath, NullLogger.Instance); var registry = new AbilityDefinitionRegistry(loaded); // Act var pulseOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var pulse); var guardOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeGuard, out var guard); var dashOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeDash, out var dash); var burstOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeBurst, out var burst); var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.True(pulseOk); Assert.NotNull(pulse); Assert.Equal(25, pulse!.BaseDamage); Assert.Equal(3.0, pulse.CooldownSeconds); Assert.True(guardOk); Assert.NotNull(guard); Assert.Equal(0, guard!.BaseDamage); Assert.Equal(6.0, guard.CooldownSeconds); Assert.True(dashOk); Assert.NotNull(dash); Assert.Equal(0, dash!.BaseDamage); Assert.Equal(4.0, dash.CooldownSeconds); Assert.True(burstOk); Assert.NotNull(burst); Assert.Equal(40, burst!.BaseDamage); Assert.Equal(5.0, burst.CooldownSeconds); Assert.Equal(7, list.Count); Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, list[0].Id); Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, list[^1].Id); } finally { try { Directory.Delete(root.FullName, recursive: true); } catch (IOException) { // Best-effort: transient lock or race on some hosts; temp dir is unique per run. } } } [Fact] public async Task Host_ShouldResolveRegistryFromDi_WhenStartupSucceeds() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); using var client = factory.CreateClient(); _ = await client.GetAsync("/health"); // Act var registry = factory.Services.GetRequiredService(); var pulseFound = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var pulse); var unknown = registry.TryGetDefinition("not_a_real_ability", out var missing); var normalized = registry.TryNormalizeKnown(" Prototype_Burst ", out var burstId); var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.True(pulseFound); Assert.NotNull(pulse); Assert.Equal(25, pulse!.BaseDamage); Assert.Equal(3.0, pulse.CooldownSeconds); Assert.False(unknown); Assert.Null(missing); Assert.True(normalized); Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, burstId); Assert.Equal(7, list.Count); Assert.Contains(list, a => a.Id == PrototypeAbilityRegistry.PrototypeGuard && a.BaseDamage == 0); } }