using FactorioCalc.Domain; using FactorioCalc.Solver; using Xunit; namespace FactorioCalc.Tests; /// /// Regression tests for critical bugs fixed in ProductionSolver. /// public class SolverBugFixTests { // --- Bug #1: mainProduct should filter by target resourceId, not .First() --- [Fact] public void Solve_MultiProductRecipe_UsesCorrectProduct() { // Simulate a recipe that produces multiple products (like oil refining) var resources = new Dictionary { { 1, new Resource(1, "Crude Oil") }, { 2, new Resource(2, "Light Oil") }, { 3, new Resource(3, "Heavy Oil") }, }; var machines = new Dictionary { { 1, new Machine(1, "Chemical Plant", 0.5, 6.0, 4, new[] { "advanced-crafting" }) }, }; // One recipe produces BOTH Light Oil and Heavy Oil var recipes = new Dictionary { { 1, new Recipe(1, "Oil Refining", "advanced-crafting", 4.0, "advanced-crafting", new[] { new Ingredient(1, 1) }, new[] { new Product(3, 1), new Product(2, 1) }) // Heavy Oil first, Light Oil second }, }; var repo = new TestRepository(recipes, resources, machines, new Dictionary()); var solver = new ProductionSolver(repo); // Target Light Oil (resourceId=2) — it's the SECOND product in the list var targets = new[] { new ProductionTarget(2, 10) }; var result = solver.Solve(targets); Assert.Single(result.Executions); // Should not throw — the solver correctly finds the matching product } // --- Bug #2: DFS visited should not block recalculation for double targets --- [Fact] public void Solve_DoubleTargetsForResource_AggregatesDemand() { 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") }, }; var machines = new Dictionary { { 3, new Machine(3, "Smelter", 0.5, 3.0, 2, new[] { "smelting" }) }, }; 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) }) }, }; var repo = new TestRepository(recipes, resources, machines, new Dictionary()); var solver = new ProductionSolver(repo); // Two targets that both need Iron Plate: Steel Plate (needs 2/sec × 2 iron plate) + direct 5/sec var targets = new[] { new ProductionTarget(9, 2), // Steel Plate → needs 4 Iron Plate/sec new ProductionTarget(7, 5), // Iron Plate direct → needs 5 more/sec }; var result = solver.Solve(targets); // Should have both Steel Plate and Iron Plate executions Assert.Equal(2, result.Executions.Count); var ironPlateExec = result.Executions.First(e => e.RecipeId == 1); // Iron Plate should account for BOTH demands (4 from steel + 5 direct = 9 total) Assert.True(ironPlateExec.MachineCount >= 9, $"Expected at least 9 machines for Iron Plate (demand=9/sec), got {ironPlateExec.MachineCount}"); } // --- Bug #3: effectiveSpeed should not go to zero with heavy productivity modules --- [Fact] public void Solve_HeavyProductivityModules_DoesNotDivideByZero() { 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 recipes = new Dictionary { { 1, new Recipe(1, "Iron Plate", "smelting", 3.0, "smelting", new[] { new Ingredient(1, 1) }, new[] { new Product(7, 1) }) }, }; // Extreme productivity modules: -20% speed × 4 slots = -80% total speed var extremeModules = new[] { new Module(1, "Prod Mod 3", ModuleType.Productivity, -0.20, 0.30, -0.15), new Module(2, "Prod Mod 3", ModuleType.Productivity, -0.20, 0.30, -0.15), new Module(3, "Prod Mod 3", ModuleType.Productivity, -0.20, 0.30, -0.15), new Module(4, "Prod Mod 3", ModuleType.Productivity, -0.20, 0.30, -0.15), }; var moduleDict = new Dictionary(); for (var i = 0; i < extremeModules.Length; i++) moduleDict[i + 1] = extremeModules[i]; var repo = new TestRepository(recipes, resources, machines, moduleDict); var solver = new ProductionSolver(repo); var targets = new[] { new ProductionTarget(7, 10) }; // Should NOT throw DivideByZeroException var result = solver.SolveWithModules(targets, extremeModules); Assert.Single(result.Executions); var exec = result.Executions.First(); // Speed should be clamped to minimum (0.05), not negative or zero Assert.True(exec.EffectiveSpeed >= 0.05, $"EffectiveSpeed {exec.EffectiveSpeed} below minimum"); Assert.True(exec.MachineCount > 0 && exec.MachineCount < int.MaxValue, $"MachineCount {exec.MachineCount} is unreasonable"); } [Fact] public void Solve_ProductivityModules_IncreasesOutput() { 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 recipes = new Dictionary { { 1, new Recipe(1, "Iron Plate", "smelting", 3.0, "smelting", new[] { new Ingredient(1, 1) }, new[] { new Product(7, 1) }) }, }; var prodModule = new Module(1, "Prod Mod 1", ModuleType.Productivity, -0.10, 0.10, -0.05); var repo = new TestRepository(recipes, resources, machines, new Dictionary { { 1, prodModule } }); var solver = new ProductionSolver(repo); var targets = new[] { new ProductionTarget(7, 10) }; var resultNoModules = solver.Solve(targets); var resultWithModules = solver.SolveWithModules(targets, new[] { prodModule }); var execNoModules = resultNoModules.Executions.First(); var execWithModules = resultWithModules.Executions.First(); // With productivity, we need fewer machines because output per cycle is higher // (even though speed is lower, the +10% output compensates) Assert.True(execWithModules.EffectiveProductivity > 0, "Productivity bonus should be positive"); } }