using FactorioCalc.Domain; using FactorioCalc.Solver; using Xunit; namespace FactorioCalc.Tests; public class SolverTests { private IRecipeRepository CreateRepository() { var resources = new Dictionary { { 1, new Resource(1, "Iron Ore") }, { 7, new Resource(7, "Iron Plate") }, { 9, new Resource(9, "Steel Plate") }, { 5, new Resource(5, "Coal") }, { 8, new Resource(8, "Copper Plate") }, { 2, new Resource(2, "Copper Ore") }, { 10, new Resource(10, "Copper Cable") }, { 11, new Resource(11, "Electronic Circuit") }, }; var machines = new Dictionary { { 3, new Machine(3, "Smelter", 0.5, 3.0, 2, new[] { "smelting" }) }, { 5, new Machine(5, "Assembler 1", 0.5, 3.0, 2, new[] { "basic-crafting" }) }, { 6, new Machine(6, "Assembler 2", 0.5, 4.0, 2, new[] { "basic-crafting" }) }, }; // Iron Plate: 1 Iron Ore -> 1 Iron Plate (3s) // Steel Plate: 2 Iron Plate + 1 Coal -> 1 Steel Plate (5s) // Copper Cable: 1 Copper Plate -> 5 Copper Cable (0.5s) // Electronic Circuit: 2 Copper Cable + 1 Iron Plate -> 1 Electronic Circuit (3s) var recipes = new Dictionary { { 1, new Recipe(1, "Iron Plate", "smelting", 3.0, "smelting", new[] { new Ingredient(1, 1) }, new[] { new Product(7, 1) }) }, { 2, new Recipe(2, "Steel Plate", "smelting", 5.0, "smelting", new[] { new Ingredient(7, 2), new Ingredient(5, 1) }, new[] { new Product(9, 1) }) }, { 3, new Recipe(3, "Copper Cable", "basic-crafting", 0.5, "basic-crafting", new[] { new Ingredient(8, 1) }, new[] { new Product(10, 5) }) }, { 4, new Recipe(4, "Electronic Circuit", "basic-crafting", 3.0, "basic-crafting", new[] { new Ingredient(10, 2), new Ingredient(7, 1) }, new[] { new Product(11, 1) }) }, }; var modules = new Dictionary(); return new TestRepository(recipes, resources, machines, modules); } [Fact] public void Solve_SingleTarget_ReturnsExecution() { var repo = CreateRepository(); var solver = new ProductionSolver(repo); // Target: 10 Iron Plate/sec var targets = new[] { new ProductionTarget(7, 10) }; var result = solver.Solve(targets); Assert.Single(result.Executions); var exec = result.Executions.First(); Assert.Equal(1, exec.RecipeId); // Iron Plate recipe Assert.Equal(3, exec.MachineId); // Smelter } [Fact] public void Solve_ChainedRecipe_ResolveDependencies() { var repo = CreateRepository(); var solver = new ProductionSolver(repo); // Target: 2 Steel Plate/sec -> needs Iron Plate -> needs Iron Ore var targets = new[] { new ProductionTarget(9, 2) }; var result = solver.Solve(targets); // Should have Steel Plate + Iron Plate executions Assert.Equal(2, result.Executions.Count); var steelExec = result.Executions.First(e => e.RecipeId == 2); Assert.NotNull(steelExec); } [Fact] public void Solve_ElectronicCircuit_FullChain() { var repo = CreateRepository(); var solver = new ProductionSolver(repo); // Target: 10 Electronic Circuit/sec -> needs Copper Cable + Iron Plate var targets = new[] { new ProductionTarget(11, 10) }; var result = solver.Solve(targets); // Should have EC + Copper Cable + Iron Plate executions Assert.Equal(3, result.Executions.Count); } [Fact] public void Solve_WithSpeedModules_IncreasesMachineCount() { var repo = CreateRepository(); var solver = new ProductionSolver(repo); var speedModule = new Module(1, "Speed Module 1", ModuleType.Speed, 0.10, 0, -0.05); var targets = new[] { new ProductionTarget(7, 10) }; var resultNoModules = solver.Solve(targets); var resultWithModules = solver.SolveWithModules(targets, new[] { speedModule }); // With speed modules, the rate per machine increases, so we might need fewer machines // or the same number with higher throughput Assert.NotEmpty(resultWithModules.Executions); } [Fact] public void Solve_WithProductivityModules_PrefersProductivityRecipe() { // This test verifies that when productivity modules are present, // the solver prefers productivity recipes if available var resources = new Dictionary { { 1, new Resource(1, "Iron Ore") }, { 7, new Resource(7, "Iron Plate") }, }; var machines = new Dictionary { { 3, new Machine(3, "Smelter", 0.5, 3.0, 2, new[] { "smelting" }) }, }; var prodModule = new Module(4, "Productivity Module 1", ModuleType.Productivity, -0.10, 0.10, -0.05); var recipes = new Dictionary { { 1, new Recipe(1, "Iron Plate", "smelting", 3.0, "smelting", new[] { new Ingredient(1, 1) }, new[] { new Product(7, 1) }) }, { 2, new Recipe(2, "Iron Plate (Productivity)", "smelting", 4.0, "smelting", new[] { new Ingredient(1, 2) }, new[] { new Product(7, 2) }) }, }; var modules = new Dictionary { { 4, prodModule } }; var repo = new TestRepository(recipes, resources, machines, modules); var solver = new ProductionSolver(repo); var targets = new[] { new ProductionTarget(7, 10) }; var result = solver.SolveWithModules(targets, new[] { prodModule }); // Should prefer the productivity recipe var exec = result.Executions.First(); Assert.Equal(2, exec.RecipeId); // Productivity recipe } [Fact] public void Solve_RawResource_NoExecution() { var repo = CreateRepository(); var solver = new ProductionSolver(repo); // Iron Ore has no recipe — it's a raw resource var targets = new[] { new ProductionTarget(1, 100) }; var result = solver.Solve(targets); Assert.Empty(result.Executions); } [Fact] public void Solve_RequiredInputs_ContainsRawResources() { var repo = CreateRepository(); var solver = new ProductionSolver(repo); // Steel Plate needs Iron Ore and Coal (both raw) var targets = new[] { new ProductionTarget(9, 1) }; var result = solver.Solve(targets); // Iron Ore (id=1) and Coal (id=5) should be in required inputs Assert.Contains(1, result.RequiredInputs.Keys); Assert.Contains(5, result.RequiredInputs.Keys); } } /// /// In-memory test repository. /// public sealed class TestRepository : IRecipeRepository { public IReadOnlyDictionary Recipes { get; } public IReadOnlyDictionary Resources { get; } public IReadOnlyDictionary Machines { get; } public IReadOnlyDictionary Modules { get; } public TestRepository( IReadOnlyDictionary recipes, IReadOnlyDictionary resources, IReadOnlyDictionary machines, IReadOnlyDictionary modules) { Recipes = recipes; Resources = resources; Machines = machines; Modules = modules; } }