using System.Collections.Frozen; using NeonSprawl.Server.Game.Crafting; using NeonSprawl.Server.Game.Items; namespace NeonSprawl.Server.Tests.Game.Crafting; public class RecipeDefinitionRegistryTests { private static readonly FrozenSet KnownItemIds = PrototypeSlice1ItemCatalogRules.ExpectedItemIds; private static readonly FrozenSet KnownSkillIds = FrozenSet.ToFrozenSet( ["salvage", "refine", "intrusion"], StringComparer.Ordinal); private static RecipeDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary byId) { var catalog = new RecipeDefinitionCatalog("/tmp/recipes", byId, catalogJsonFileCount: 1); return new RecipeDefinitionRegistry(catalog); } [Fact] public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenRefineRecipeExists() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { ["refine_scrap_standard"] = new RecipeDefRow( "refine_scrap_standard", "Refine Scrap (Standard)", "process", "refine", [new RecipeIoRow("scrap_metal_bulk", 5)], [new RecipeIoRow("refined_plate_stock", 1)], RequiredSkillLevel: null, StationTag: null), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition("refine_scrap_standard", out var def); // Assert Assert.True(found); Assert.NotNull(def); Assert.Equal("process", def.RecipeKind); Assert.Equal("refine", def.RequiredSkillId); Assert.Equal("Refine Scrap (Standard)", def.DisplayName); Assert.Single(def.Inputs); Assert.Equal("scrap_metal_bulk", def.Inputs[0].ItemId); Assert.Equal(5, def.Inputs[0].Quantity); Assert.Single(def.Outputs); Assert.Equal("refined_plate_stock", def.Outputs[0].ItemId); Assert.Equal(1, def.Outputs[0].Quantity); } [Fact] public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenMakeRecipeExists() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { ["make_field_stim_mk0"] = new RecipeDefRow( "make_field_stim_mk0", "Make Field Stim Mk0", "make", "refine", [ new RecipeIoRow("refined_plate_stock", 2), new RecipeIoRow("scrap_metal_bulk", 1), ], [new RecipeIoRow("field_stim_mk0", 1)], RequiredSkillLevel: null, StationTag: null), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition("make_field_stim_mk0", out var def); // Assert Assert.True(found); Assert.NotNull(def); Assert.Equal("make", def.RecipeKind); Assert.Equal(2, def.Inputs.Count); Assert.Equal("refined_plate_stock", def.Inputs[0].ItemId); Assert.Equal("scrap_metal_bulk", def.Inputs[1].ItemId); Assert.Single(def.Outputs); Assert.Equal("field_stim_mk0", def.Outputs[0].ItemId); } [Fact] public void TryGetDefinition_ShouldReturnFalse_WhenRecipeIdIsNull() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { ["refine_scrap_standard"] = new RecipeDefRow( "refine_scrap_standard", "Refine Scrap (Standard)", "process", "refine", [new RecipeIoRow("scrap_metal_bulk", 5)], [new RecipeIoRow("refined_plate_stock", 1)], RequiredSkillLevel: null, StationTag: null), }; 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) { ["refine_scrap_standard"] = new RecipeDefRow( "refine_scrap_standard", "Refine Scrap (Standard)", "process", "refine", [new RecipeIoRow("scrap_metal_bulk", 5)], [new RecipeIoRow("refined_plate_stock", 1)], RequiredSkillLevel: null, StationTag: null), }; var registry = CreateRegistryFromRows(rows); // Act var found = registry.TryGetDefinition("not_a_real_recipe", out var def); // Assert Assert.False(found); Assert.Null(def); } [Fact] public void GetDefinitionsInIdOrder_ShouldListAllRowsOrderedById_WhenMultipleRecipes() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { ["make_field_stim_mk0"] = new RecipeDefRow( "make_field_stim_mk0", "Make Field Stim Mk0", "make", "refine", [new RecipeIoRow("refined_plate_stock", 2)], [new RecipeIoRow("field_stim_mk0", 1)], RequiredSkillLevel: null, StationTag: null), ["refine_scrap_efficient"] = new RecipeDefRow( "refine_scrap_efficient", "Refine Scrap (Efficient)", "process", "refine", [new RecipeIoRow("scrap_metal_bulk", 10)], [new RecipeIoRow("refined_plate_stock", 2)], RequiredSkillLevel: null, StationTag: null), ["refine_scrap_standard"] = new RecipeDefRow( "refine_scrap_standard", "Refine Scrap (Standard)", "process", "refine", [new RecipeIoRow("scrap_metal_bulk", 5)], [new RecipeIoRow("refined_plate_stock", 1)], RequiredSkillLevel: null, StationTag: null), }; var registry = CreateRegistryFromRows(rows); // Act var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.Equal(3, list.Count); Assert.Equal("make_field_stim_mk0", list[0].Id); Assert.Equal("refine_scrap_efficient", list[1].Id); Assert.Equal("refine_scrap_standard", list[2].Id); } [Fact] public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture() { // Arrange var root = Directory.CreateTempSubdirectory("neon-sprawl-recipe-registry-loader-"); try { var recipesDir = Path.Combine(root.FullName, "content", "recipes"); var schemaDir = Path.Combine(root.FullName, "content", "schemas"); Directory.CreateDirectory(recipesDir); Directory.CreateDirectory(schemaDir); var recipeDefSchemaPath = Path.Combine(schemaDir, "recipe-def.schema.json"); var recipeIoRowSchemaPath = Path.Combine(schemaDir, "recipe-io-row.schema.json"); File.Copy(RecipeCatalogTestPaths.DiscoverRepoRecipeDefSchemaPath(), recipeDefSchemaPath, overwrite: true); File.Copy(RecipeCatalogTestPaths.DiscoverRepoRecipeIoRowSchemaPath(), recipeIoRowSchemaPath, overwrite: true); File.Copy( Path.Combine(RecipeCatalogTestPaths.DiscoverRepoRecipesDirectory(), "prototype_recipes.json"), Path.Combine(recipesDir, "prototype_recipes.json"), overwrite: true); var loaded = RecipeDefinitionCatalogLoader.Load( recipesDir, recipeDefSchemaPath, recipeIoRowSchemaPath, KnownItemIds, KnownSkillIds, NullLogger.Instance); var registry = new RecipeDefinitionRegistry(loaded); // Act var ok = registry.TryGetDefinition("refine_scrap_standard", out var refine); var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.True(ok); Assert.NotNull(refine); Assert.Equal("process", refine.RecipeKind); Assert.Equal("refine", refine.RequiredSkillId); Assert.Equal(8, list.Count); Assert.Equal("make_armor_quick", list[0].Id); Assert.Equal("refine_scrap_standard", 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 found = registry.TryGetDefinition("refine_scrap_standard", out var refine); var unknown = registry.TryGetDefinition("not_a_real_recipe", out var missing); var list = registry.GetDefinitionsInIdOrder(); // Assert Assert.True(found); Assert.NotNull(refine); Assert.Equal("process", refine.RecipeKind); Assert.False(unknown); Assert.Null(missing); Assert.Equal(8, list.Count); Assert.Contains(list, r => r.Id == "make_field_stim_mk0" && r.RecipeKind == "make"); } }