using FactorioCalc.Domain;
using FactorioCalc.Solver;
using Xunit;
namespace FactorioCalc.Tests;
///
/// Tests for edge cases and correctness of the matrix-based solver.
///
public class SolverBugFixTests
{
// --- Multi-product recipes (e.g. oil refining) ---
[Fact]
public void Solve_MultiProductRecipe_UsesCorrectProduct()
{
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) })
},
};
var repo = new TestRepository(recipes, resources, machines, new Dictionary());
var solver = new ProductionSolver(repo);
// Target Light Oil — it's the SECOND product
var targets = new[] { new ProductionTarget(2, 10) };
var result = solver.Solve(targets);
Assert.Single(result.Executions);
// Heavy Oil should appear as a byproduct in resource flows
Assert.True(result.ResourceFlows.TryGetValue(3, out var heavyOilFlow));
Assert.True(heavyOilFlow > 0, "Heavy Oil should be produced as byproduct");
}
// --- Double targets aggregate correctly via matrix ---
[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);
var targets = new[]
{
new ProductionTarget(9, 2), // Steel Plate → needs 4 Iron Plate/sec
new ProductionTarget(7, 5), // Iron Plate direct → 5 more/sec
};
var result = solver.Solve(targets);
Assert.Equal(2, result.Executions.Count);
var ironPlateExec = result.Executions.First(e => e.RecipeId == 1);
Assert.True(ironPlateExec.MachineCount >= 9,
$"Expected at least 9 machines for Iron Plate (demand=9/sec), got {ironPlateExec.MachineCount}");
}
// --- Heavy productivity modules don't cause division by zero ---
[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: -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
var result = solver.SolveWithModules(targets, extremeModules);
Assert.Single(result.Executions);
var exec = result.Executions.First();
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 resultWithModules = solver.SolveWithModules(targets, new[] { prodModule });
var execWithModules = resultWithModules.Executions.First();
Assert.True(execWithModules.EffectiveProductivity > 0, "Productivity bonus should be positive");
}
// --- Matrix solver correctness: verify exact rates ---
[Fact]
public void Solve_SimpleChain_ExactRates()
{
// Iron Plate: 1 ore → 1 plate (3s), speed=0.5
// Steel Plate: 2 plates + 1 coal → 1 steel (5s), speed=0.5
// Target: 1 steel/sec
// → Steel recipe rate = 1 cycle/sec
// → needs 2 iron plate/sec
// → Iron Plate recipe rate = 2 cycles/sec
// → needs 2 iron ore/sec
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);
var targets = new[] { new ProductionTarget(9, 1) };
var result = solver.Solve(targets);
var steelExec = result.Executions.First(e => e.RecipeId == 2);
var ironExec = result.Executions.First(e => e.RecipeId == 1);
// Steel Plate rate should be ~1 cycle/sec
Assert.InRange(steelExec.RecipeRate, 0.99, 1.01);
// Iron Plate rate should be ~2 cycles/sec
Assert.InRange(ironExec.RecipeRate, 1.99, 2.01);
}
// --- ResourceClassifier tests ---
[Fact]
public void ResourceClassifier_IdentifiesRawAndCraftable()
{
var resources = new Dictionary
{
{ 1, new Resource(1, "Iron Ore") },
{ 7, new Resource(7, "Iron Plate") },
};
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 machines = new Dictionary();
var repo = new TestRepository(recipes, resources, machines, new Dictionary());
var classifier = new ResourceClassifier();
classifier.Classify(repo.Recipes, new[] { new ProductionTarget(7, 10) });
Assert.True(classifier.Craftable.Contains(7));
Assert.True(classifier.Raw.Contains(1));
Assert.True(classifier.Targets.Contains(7));
}
// --- RecipeMatrixBuilder tests ---
[Fact]
public void RecipeMatrixBuilder_BuildsSquareMatrix()
{
var resources = new Dictionary
{
{ 1, new Resource(1, "Iron Ore") },
{ 7, new Resource(7, "Iron Plate") },
};
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 machines = new Dictionary();
var repo = new TestRepository(recipes, resources, machines, new Dictionary());
var classifier = new ResourceClassifier();
classifier.Classify(repo.Recipes, new[] { new ProductionTarget(7, 10) });
var builder = new RecipeMatrixBuilder();
builder.Build(repo.Recipes, new[] { new ProductionTarget(7, 10) }, classifier);
// Single craftable resource, single recipe → 1×1 matrix
Assert.Equal(1, builder.Matrix.RowCount);
Assert.Equal(1, builder.Matrix.ColumnCount);
Assert.Equal(10, builder.RightHandSide[0]); // target = 10/sec
Assert.Equal(1, builder.Matrix[0, 0]); // +1 Iron Plate per cycle
}
}