using System.IO; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using NeonSprawl.Server.Game.Npc; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.Npc; public class NpcBehaviorDefinitionRegistryTests { private static NpcBehaviorDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary byId) { var catalog = new NpcBehaviorDefinitionCatalog("/tmp/npc-behaviors", byId, catalogJsonFileCount: 1); return new NpcBehaviorDefinitionRegistry(catalog); } private static NpcBehaviorDefRow MeleeRow() => new( PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, "Melee Pressure", "melee_pressure", 100, 8.0, 16.0, 1.5, 15, 3.0); private static NpcBehaviorDefRow RangedRow() => new( PrototypeNpcBehaviorRegistry.PrototypeRangedControl, "Ranged Control", "ranged_control", 80, 10.0, 20.0, 2.0, 12, 4.0); private static NpcBehaviorDefRow EliteRow() => new( PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, "Elite Mini-Boss", "elite_mini_boss", 200, 8.0, 18.0, 2.5, 25, 5.0); [Fact] public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenMeleeExists() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, out var def); // Assert Assert.True(found); Assert.NotNull(def); Assert.Equal(100, def.MaxHp); Assert.Equal(8.0, def.AggroRadius); Assert.Equal(16.0, def.LeashRadius); Assert.Equal(15, def.AttackDamage); Assert.Equal(1.5, def.TelegraphWindupSeconds); Assert.Equal("melee_pressure", def.ArchetypeKind); Assert.Equal("Melee Pressure", def.DisplayName); } [Fact] public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenEliteExists() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss] = EliteRow(), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, out var def); // Assert Assert.True(found); Assert.NotNull(def); Assert.Equal(200, def.MaxHp); Assert.Equal(25, def.AttackDamage); Assert.Equal(2.5, def.TelegraphWindupSeconds); } [Fact] public void TryGetDefinition_ShouldReturnFalse_WhenBehaviorIdIsNull() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; 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) { [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition("not_a_real_behavior", out var def); // Assert Assert.False(found); Assert.Null(def); } [Fact] public void TryGetDefinition_ShouldReturnFalse_WhenIdHasMixedCase() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition("Prototype_Melee_Pressure", out var def); // Assert Assert.False(found); Assert.Null(def); } [Fact] public void TryNormalizeKnown_ShouldReturnTrueAndLowercaseId_WhenInputHasWhitespaceAndMixedCase() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryNormalizeKnown(" Prototype_Melee_Pressure ", out var normalized); // Assert Assert.True(found); Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, normalized); } [Fact] public void TryNormalizeKnown_ShouldReturnFalse_WhenInputIsNull() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; 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) { [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; 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) { [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; 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_WhenMultipleBehaviors() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { [PrototypeNpcBehaviorRegistry.PrototypeRangedControl] = RangedRow(), [PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss] = EliteRow(), [PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(), }; var registry = CreateRegistryFromRows(rows); // Act var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.Equal(3, list.Count); Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, list[0].Id); Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, list[1].Id); Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeRangedControl, list[2].Id); } [Fact] public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture() { // Arrange var root = Directory.CreateTempSubdirectory("neon-sprawl-npc-registry-loader-"); try { var npcBehaviorsDir = Path.Combine(root.FullName, "content", "npc-behaviors"); var schemaDir = Path.Combine(root.FullName, "content", "schemas"); Directory.CreateDirectory(npcBehaviorsDir); Directory.CreateDirectory(schemaDir); var schemaPath = Path.Combine(schemaDir, "npc-behavior-def.schema.json"); File.Copy(NpcBehaviorCatalogTestPaths.DiscoverRepoNpcBehaviorDefSchemaPath(), schemaPath, overwrite: true); File.Copy( Path.Combine(NpcBehaviorCatalogTestPaths.DiscoverRepoNpcBehaviorsDirectory(), "prototype_npc_behaviors.json"), Path.Combine(npcBehaviorsDir, "prototype_npc_behaviors.json"), overwrite: true); var loaded = NpcBehaviorDefinitionCatalogLoader.Load(npcBehaviorsDir, schemaPath, NullLogger.Instance); var registry = new NpcBehaviorDefinitionRegistry(loaded); // Act var meleeOk = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, out var melee); var rangedOk = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeRangedControl, out var ranged); var eliteOk = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, out var elite); var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.True(meleeOk); Assert.NotNull(melee); Assert.Equal(100, melee!.MaxHp); Assert.Equal(8.0, melee.AggroRadius); Assert.Equal(16.0, melee.LeashRadius); Assert.Equal(15, melee.AttackDamage); Assert.Equal(1.5, melee.TelegraphWindupSeconds); Assert.True(rangedOk); Assert.NotNull(ranged); Assert.Equal(80, ranged!.MaxHp); Assert.Equal(10.0, ranged.AggroRadius); Assert.Equal(20.0, ranged.LeashRadius); Assert.Equal(12, ranged.AttackDamage); Assert.Equal(2.0, ranged.TelegraphWindupSeconds); Assert.True(eliteOk); Assert.NotNull(elite); Assert.Equal(200, elite!.MaxHp); Assert.Equal(8.0, elite.AggroRadius); Assert.Equal(18.0, elite.LeashRadius); Assert.Equal(25, elite.AttackDamage); Assert.Equal(2.5, elite.TelegraphWindupSeconds); Assert.Equal(3, list.Count); Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, list[0].Id); Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeRangedControl, 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 meleeFound = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, out var melee); var unknown = registry.TryGetDefinition("not_a_real_behavior", out var missing); var normalized = registry.TryNormalizeKnown(" Prototype_Ranged_Control ", out var rangedId); var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.True(meleeFound); Assert.NotNull(melee); Assert.Equal(100, melee!.MaxHp); Assert.Equal(15, melee.AttackDamage); Assert.False(unknown); Assert.Null(missing); Assert.True(normalized); Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeRangedControl, rangedId); Assert.Equal(3, list.Count); Assert.Contains( list, b => b.Id == PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss && b.MaxHp == 200 && b.AttackDamage == 25); } }