import assert from "node:assert/strict";
import test from "node:test";

import {
  calculateBeaconDesign,
  calculateBiolabBeaconDesign,
  calculateRocketSiloBeaconDesign,
} from "../app/lib/beacons.ts";
import {
  beltCapacityPerSecond,
  calculateBeltLoad,
} from "../app/lib/belts.ts";
import { calculate, effectsForModules, qualityDistribution, resolveAutoModules } from "../app/lib/calculator.ts";
import { calculateEnergy } from "../app/lib/energy.ts";
import {
  buildFactoryTotals,
  mostUsedRateUnit,
} from "../app/lib/factory-totals.ts";
import {
  DEFAULT_MACHINE_PRIORITIES,
  DEFAULT_UNLOCKED_MACHINE_IDS,
  rankedMachinesForRecipe,
} from "../app/lib/chain-machines.ts";
import {
  DATA_META, MACHINES, MACHINE_BY_ID, MODULES, PRODUCTS, PRODUCT_BY_KEY, QUALITIES,
  RECIPES, RECIPE_BY_ID, downstreamProducts, machinesForRecipe,
  preferredChainRecipeForProduct, preferredRecipeForProduct,
} from "../app/lib/factorio-data.ts";
import {
  languageLabel,
  localizedName,
  tr,
} from "../app/lib/i18n.ts";
import { calculateLogistics, DEFAULT_LOGISTICS } from "../app/lib/logistics.ts";
import {
  miningSourceForBaseline,
  resolveMiningDrill,
} from "../app/lib/mining.ts";
import { buildProductionChain } from "../app/lib/production-chain.ts";
import {
  QUALITY_RECYCLING_PRODUCTS,
  RECYCLING_PRODUCTS,
  RECYCLING_ROUTES,
  RECYCLING_ROUTE_BY_ID,
  calculateRecycling,
} from "../app/lib/recycling.ts";
import { LABS, calculateScience } from "../app/lib/science.ts";
import {
  ROCKET_LOCATIONS,
  ROCKET_PRODUCTS,
  ROCKET_PRODUCT_BY_ID,
  calculateRocketFreight,
} from "../app/lib/rocket-freight.ts";

function close(actual, expected, epsilon = 1e-9) {
  assert.ok(Math.abs(actual - expected) <= epsilon, `${actual} should equal ${expected}`);
}
function setup(recipeId, productKey, machineId, overrides = {}) {
  const recipe = RECIPE_BY_ID.get(recipeId);
  const product = PRODUCT_BY_KEY.get(productKey);
  const machine = MACHINE_BY_ID.get(machineId);
  assert.ok(recipe && product && machine);
  return {
    recipe, product, machine, machineQuality: "normal", inputQuality: "normal",
    outputQuality: "total", maxQuality: "legendary", calculationMode: "normal",
    machineModules: Array.from({ length: machine.moduleSlots }, () => null),
    beaconModules: [null, null], beaconQuality: "normal", beaconCount: 0,
    mode: "output", amount: 60, unit: "s", inputConstraints: [], ...overrides,
  };
}

test("catalog exposes the official recipes plus every passive spoilage route", () => {
  assert.equal(DATA_META.factorioVersion, "2.1.17");
  assert.equal(DATA_META.prototypeRecipes, 314);
  assert.equal(DATA_META.spoilageRoutes, 10);
  assert.equal(DATA_META.visibleRecipes, 324);
  assert.equal(DATA_META.products, 282);
  assert.equal(RECIPE_BY_ID.has("parameter-0"), false);
  assert.ok(RECIPE_BY_ID.has("spoil-copper-bacteria"));
  assert.ok(RECIPE_BY_ID.has("spoil-iron-bacteria"));
});

test("all product route and machine combinations are finite and net-positive", () => {
  let calculations = 0;
  for (const product of PRODUCTS) {
    for (const recipeId of product.recipeIds) {
      const recipe = RECIPE_BY_ID.get(recipeId);
      assert.ok(recipe);
      const machines = machinesForRecipe(recipe);
      assert.ok(machines.length, recipe.id);
      for (const machine of machines) {
        const result = calculate(setup(recipe.id, product.key, machine.id, { amount: 1 }));
        assert.equal(result.valid, true, `${recipe.id} → ${product.key} in ${machine.id}: ${result.message}`);
        assert.ok(Number.isFinite(result.ratePerSecond));
        calculations += 1;
      }
    }
  }
  assert.equal(calculations, 846);
});

test("all bundled catalog entity and effect icons resolve locally", async () => {
  const { access } = await import("node:fs/promises");
  const icons = new Set([
    ...[...RECIPES, ...PRODUCTS, ...MACHINES, ...MODULES, ...QUALITIES].map((entry) => entry.icon),
    ...RECYCLING_ROUTES.map((route) => route.productIcon),
  ]);
  for (const icon of icons) {
    assert.notEqual(icon, "/favicon.svg", icon);
    await access(new URL(`../public${icon}`, import.meta.url));
  }
});

test("inventory-style product groups are complete", () => {
  const counts = Object.fromEntries(["logistics","production","intermediate-products","space","combat","fluids"].map((group) => [group, PRODUCTS.filter((p) => p.group === group).length]));
  assert.deepEqual(counts, { logistics: 71, production: 50, "intermediate-products": 69, space: 12, combat: 64, fluids: 16 });
});

test("official locale catalogs cover every product and machine in all nine languages", () => {
  const locales = ["en","de","es-ES","fr","it","ja","ru","zh-CN","zh-TW"];
  for (const locale of locales) {
    for (const product of PRODUCTS) assert.ok(localizedName(product.id, locale, product.type).length > 0);
    for (const machine of MACHINES) assert.ok(localizedName(machine.id, locale, "entity").length > 0);
  }
  assert.equal(localizedName("advanced-circuit", "de", "item"), "Integrierter Schaltkreis");
  assert.equal(localizedName("assembling-machine-3", "fr", "entity"), "Machine d'assemblage très rapide");
  assert.equal(localizedName("electromagnetic-plant", "ja", "entity"), "EMP");
});

test("new interface copy and language names are localized in all nine languages", () => {
  const locales = ["en", "de", "es-ES", "fr", "it", "ja", "ru", "zh-CN", "zh-TW"];
  const keys = [
    "rocket",
    "currentProducts",
    "resetCurrent",
    "optimizedLayout",
    "energyBlueprint",
    "orientation",
    "rocketCapacity",
    "platformSpeed",
    "preloadedNotice",
    "launchesPerMinute",
    "landingWaves",
    "tiles",
    "rocketSelectionError",
    "thrusterEstimateNotice",
    "rocketSilosToPlace",
    "siloLoading",
    "experimentalInserterLoading",
    "payloadInsertion",
    "steamConsumer",
    "highTemperatureLoss",
    "downstreamProducts",
    "targetReturn",
    "desiredQualityOutput",
    "chainMaster",
    "applyEntireChain",
    "machinePriority",
    "modulePriority",
    "rocketPayloadBottleneck",
    "cargoStorageBottleneck",
    "flightCalculation",
    "launchCalculation",
    "inventoryTabs",
    "qualityBeaconMode",
    "cargoBayQuality",
    "ignoreInsertion",
    "globalChainMaster",
    "applyAllActiveChains",
    "subprocessMachines",
    "subprocessMachineHint",
    "insertersRequired",
    "moveProductToTop",
    "factoryTotals",
    "factoryTotalsHint",
    "combinedResources",
    "productionProcesses",
    "directInputs",
    "collapseProductResult",
    "expandProductResult",
    "allChainsGenerated",
    "includeProductionChain",
    "includeProductionChainHint",
    "chainInserterHint",
    "beltThroughput",
    "beltThroughputHint",
    "beltType",
    "beltStacking",
    "unstacked",
    "stackInserterLevel",
    "beltCapacityOne",
    "beltCapacityTwo",
    "splitSharedStreams",
    "splitSharedStreamsHint",
    "beltCapacity",
    "beltEquivalents",
    "beltsRequired",
    "lastBeltLoad",
    "combinedStream",
    "insertersPerMachine",
    "experimentalRobotLoading",
    "ignoreInsertionWarning",
    "logisticRobotCount",
    "workerRobotSpeedResearch",
    "workerRobotCargoResearch",
    "robotFlightTime",
    "robotSupplyTime",
    "bufferChestAssumption",
    "payloadWaveTitle",
    "payloadWaveExplanation",
    "machineResearchAndPriority",
    "machineResearchAndPriorityHint",
    "unlockedMachines",
    "machineHierarchy",
    "machineGroupCrafting",
    "machineGroupSmelting",
    "machineGroupFluids",
    "machineGroupBiology",
    "machineGroupProcessing",
    "totalBeltLoad",
    "constructionSummary",
    "constructionSummaryHint",
    "buildKindMachine",
    "buildKindModule",
    "buildKindBeacon",
    "buildKindInserter",
    "miningDrill",
    "miningDrillHint",
    "miningAndRawQuality",
    "miningModules",
    "qualityMiningHint",
    "miningQualityChance",
    "occupiedSlots",
    "slotsPerBeacon",
  ];
  for (const locale of locales) {
    for (const key of keys) assert.ok(tr(locale, key).trim().length > 0, `${locale}:${key}`);
    for (const target of locales) assert.ok(languageLabel(target, locale).trim().length > 0);
  }
  assert.notEqual(languageLabel("de", "en"), languageLabel("de", "de"));
});

test("advanced circuits retain EMP base productivity and exact balances", () => {
  const result = calculate(setup("advanced-circuit", "item:advanced-circuit", "electromagnetic-plant"));
  close(result.cyclesPerSecond, 40);
  close(result.productivityBonus, .5);
  close(result.theoreticalMachines, 120);
  assert.deepEqual(Object.fromEntries(result.inputs.map((f) => [f.id, f.total])),
    { "electronic-circuit": 80, "plastic-bar": 80, "copper-cable": 160 });
  close(result.electricTargetKw, 248000, 1e-6);
});

test("Gleba operating fuels are material inputs and net against returned nutrients", () => {
  const mash = calculate(setup("yumako-processing", "item:yumako-mash", "biochamber", { amount: 1 }));
  close(mash.inputs.find((f) => f.id === "nutrients").total, 1 / 24);
  const eggs = calculate(setup("biter-egg", "item:biter-egg", "captive-biter-spawner", { amount: 1 }));
  close(eggs.inputs.find((f) => f.id === "bioflux").total, 1 / 30);
  const nutrients = calculate(setup("nutrients-from-yumako-mash", "item:nutrients", "biochamber", { amount: 1 }));
  assert.equal(nutrients.inputs.some((f) => f.id === "nutrients"), false);
  close(nutrients.inputs.find((f) => f.id === "yumako-mash").total, 8 / 17);
  close(nutrients.outputs.find((f) => f.id === "nutrients").total, 1);
});

test("input-limited fluids catalysts and probabilities remain valid", () => {
  const limited = calculate(setup("advanced-circuit", "item:advanced-circuit", "electromagnetic-plant", {
    mode: "input", amount: 0,
    inputConstraints: [{ id: "plastic", ingredientKey: "item:plastic-bar", amount: 60 }],
  }));
  close(limited.ratePerSecond, 45);
  const kovarex = calculate(setup("kovarex-enrichment-process", "item:uranium-235", "centrifuge", { amount: 1 }));
  assert.deepEqual(kovarex.inputs.map((f) => [f.id, f.total]), [["uranium-238", 3]]);
  const oil = calculate(setup("advanced-oil-processing", "fluid:petroleum-gas", "oil-refinery", { amount: 110 }));
  assert.deepEqual(Object.fromEntries(oil.inputs.map((f) => [f.id, f.total])), { water: 100, "crude-oil": 200 });
  const scrap = calculate(setup("scrap-recycling", "item:processing-unit", "recycler", { amount: 1 }));
  close(scrap.targetPerCycle, .02);
});

test("module scaling priority and quality distribution match 2.1 behavior", () => {
  const recipe = RECIPE_BY_ID.get("advanced-circuit");
  const machine = MACHINE_BY_ID.get("electromagnetic-plant");
  const speed = effectsForModules([{ family: "speed", tier: 3, quality: "legendary" }], machine, recipe);
  close(speed.speed, 1.25); close(speed.consumption, .7); close(speed.quality, -.025);
  const modules = resolveAutoModules(["efficiency","speed","productivity"], 3, "normal", machine.moduleSlots, machine, recipe);
  assert.ok(modules.every((selection) => selection.family === "productivity"));
  const values = Object.fromEntries(qualityDistribution("normal", .25).map((x) => [x.quality, x.amount]));
  close(values.normal, .75); close(values.uncommon, .225); close(values.legendary, .00025);
  const capped = Object.fromEntries(qualityDistribution("normal", .25, "epic").map((x) => [x.quality, x.amount]));
  close(capped.epic, .0025);
});

test("quality production is explicit and includes speed-beacon quality penalties", () => {
  const modules = Array.from({ length: 5 }, () => ({ family: "quality", tier: 3, quality: "legendary" }));
  const quality = calculate(setup("advanced-circuit", "item:advanced-circuit", "electromagnetic-plant", {
    machineQuality: "legendary", machineModules: modules, calculationMode: "quality",
  }));
  close(quality.qualityChance, .3125);
  close(quality.qualityDistribution.reduce((s, x) => s + x.amount, 0), 60);
  const speedBeacon = calculate(setup("advanced-circuit", "item:advanced-circuit", "electromagnetic-plant", {
    machineModules: Array.from({ length: 5 }, () => ({ family: "quality", tier: 3, quality: "normal" })),
    calculationMode: "quality", beaconCount: 1,
    beaconModules: [{ family: "speed", tier: 3, quality: "normal" }, null],
  }));
  close(speedBeacon.qualityChance, .0875);
});

test("tile geometry enforces corridor and physical maxima", () => {
  const expected = { "assembling-machine-3": 12, "electromagnetic-plant": 12, "oil-refinery": 16, "rocket-silo": 16 };
  for (const [id, max] of Object.entries(expected)) {
    const machine = MACHINE_BY_ID.get(id);
    const single = calculateBeaconDesign(machine, 1, 99);
    assert.equal(single.counts[0], max);
    const machineRect = single.placements.find((p) => p.kind === "machine");
    for (const beacon of single.placements.filter((p) => p.kind === "beacon")) {
      const horizontalGap = Math.max(machineRect.x - (beacon.x + beacon.width), beacon.x - (machineRect.x + machineRect.width), 0);
      const verticalGap = Math.max(machineRect.y - (beacon.y + beacon.height), beacon.y - (machineRect.y + machineRect.height), 0);
      const overlapsMachine = beacon.x < machineRect.x + machineRect.width && beacon.x + beacon.width > machineRect.x && beacon.y < machineRect.y + machineRect.height && beacon.y + beacon.height > machineRect.y;
      assert.equal(overlapsMachine, false);
      if (horizontalGap === 0) assert.ok(verticalGap === 0 || verticalGap >= 2);
    }
  }
  const row = calculateBeaconDesign(MACHINE_BY_ID.get("assembling-machine-3"), 7, 12);
  assert.equal(row.machineCount, 7);
  assert.ok(row.average < 12);
  assert.equal(row.counts.length, 7);

  const silo = calculateBeaconDesign(MACHINE_BY_ID.get("rocket-silo"), 1, 16);
  assert.equal(silo.corridor, 0);
  assert.equal(silo.uniqueBeacons, 16);
  const siloRect = silo.placements.find((placement) => placement.kind === "machine");
  for (const beacon of silo.placements.filter((placement) => placement.kind === "beacon")) {
    const horizontalGap = Math.max(siloRect.x - (beacon.x + beacon.width), beacon.x - (siloRect.x + siloRect.width), 0);
    const verticalGap = Math.max(siloRect.y - (beacon.y + beacon.height), beacon.y - (siloRect.y + siloRect.height), 0);
    assert.ok(horizontalGap === 0 || verticalGap === 0, "silo beacons must be flush on one axis");
    assert.ok(beacon.x >= siloRect.x - beacon.width, "silo ring must not leave a detached beacon at the left edge");
    assert.ok(beacon.x <= siloRect.x + siloRect.width, "silo ring must not leave a detached beacon at the right edge");
  }
});

test("Biolabs use one continuous shared 16-beacon row but can switch off", () => {
  const on = calculateScience({
    labId: "biolab", labQuality: "normal", packQuality: "normal", mode: "effective",
    amountPerMinute: 60, machineModules: [null,null,null,null], beaconEnabled: true,
    beaconCount: 3, biolabBeaconMode: "optimized", beaconModules: [null,null], beaconQuality: "normal",
  });
  assert.equal(on.forcedBiolabRing, true);
  assert.ok(on.beaconLayout.counts.every((count) => count === 16));
  assert.ok(on.beaconLayout.uniqueBeacons < on.labsToBuild * 16);
  const off = calculateScience({
    labId: "biolab", labQuality: "normal", packQuality: "normal", mode: "effective",
    amountPerMinute: 60, machineModules: [null,null,null,null], beaconEnabled: false,
    beaconCount: 16, biolabBeaconMode: "optimized", beaconModules: [null,null], beaconQuality: "normal",
  });
  assert.equal(off.beaconLayout.uniqueBeacons, 0);
  const layout = calculateBiolabBeaconDesign(LABS.biolab, 6);
  assert.deepEqual(layout.counts, [16, 16, 16, 16, 16, 16]);
  assert.equal(layout.uniqueBeacons, 71);
  const machines = layout.placements.filter((placement) => placement.kind === "machine");
  const beacons = layout.placements.filter((placement) => placement.kind === "beacon");
  assert.deepEqual(machines.map((placement) => placement.x), [0, 12, 24, 36, 48, 60]);
  assert.deepEqual(
    machines.map((_, index) => beacons.filter((beacon) => beacon.affects?.includes(index)).length),
    [16, 16, 16, 16, 16, 16],
  );
  assert.ok(beacons.some((beacon) => beacon.x === 7 && beacon.affects?.includes(0) && beacon.affects?.includes(1)));
  for (const [index, machine] of machines.entries()) {
    for (const beacon of beacons.filter((placement) => placement.affects?.includes(index))) {
      const horizontalGap = Math.max(machine.x - (beacon.x + beacon.width), beacon.x - (machine.x + machine.width), 0);
      const verticalGap = Math.max(machine.y - (beacon.y + beacon.height), beacon.y - (machine.y + machine.height), 0);
      assert.ok(horizontalGap === 2 || verticalGap === 2, "Biolab beacons retain a two-tile gap");
    }
  }
});

test("science solves supplied packs and target research in both directions", () => {
  const productivity = { family: "productivity", tier: 3, quality: "legendary" };
  const speed = { family: "speed", tier: 3, quality: "legendary" };
  const common = {
    labId: "biolab", labQuality: "legendary", packQuality: "normal",
    machineModules: [productivity, productivity, productivity, productivity],
    beaconEnabled: true, beaconCount: 16, biolabBeaconMode: "manual",
    beaconModules: [speed, speed], beaconQuality: "legendary",
  };
  const supplied = calculateScience({ ...common, mode: "actual", amountPerMinute: 14_400 });
  close(supplied.actualSpm, 14_400);
  close(supplied.effectiveSpm, 57_600);
  close(supplied.effectivePerPack, 4);
  assert.equal(supplied.labsToBuild, 227);

  const targeted = calculateScience({ ...common, mode: "effective", amountPerMinute: 57_600 });
  close(targeted.actualSpm, supplied.actualSpm);
  close(targeted.effectiveSpm, supplied.effectiveSpm);
  close(targeted.labsTheoretical, supplied.labsTheoretical);

  const legendaryPacks = calculateScience({
    ...common, packQuality: "legendary", mode: "actual", amountPerMinute: 14_400,
  });
  close(legendaryPacks.effectiveSpm, 345_600);
  close(legendaryPacks.effectivePerPack, 24);
});

test("recycler rows are gapless and quality defaults share exact one-beacon coverage", () => {
  const machine = MACHINE_BY_ID.get("recycler");
  const layout = calculateBeaconDesign(machine, 6, 1);
  const machines = layout.placements.filter((placement) => placement.kind === "machine");
  assert.deepEqual(machines.map((placement) => placement.x), [0, 2, 4, 6, 8, 10]);
  assert.deepEqual(layout.counts, [1, 1, 1, 1, 1, 1]);
  assert.ok(layout.uniqueBeacons < machines.length);
});

test("fusion fixed blocks and demand sizing expose gross internal and net power", () => {
  for (const [reactors, generators, gross, internal, expectedLinks] of [[1,2,100,11.55,0],[2,8,400,23.1,1],[3,18,900,34.65,3],[4,28,1400,46.2,5]]) {
    const result = calculateEnergy({ targetMw: 0, technology: "fusion", quality: "normal", reservePercent: 0, planet: "nauvis", nuclearWidth: 2, nuclearHeight: 2, fusionMode: "fixed", fusionReactors: reactors });
    assert.equal(result.fusion.generators, generators); close(result.fusion.grossMw, gross); close(result.fusion.internalMw, internal);
    const placedReactors = result.blueprint.placements.filter((entry) => entry.entityId === "fusion-reactor");
    let links = 0;
    for (let first = 0; first < placedReactors.length; first += 1) {
      for (let second = first + 1; second < placedReactors.length; second += 1) {
        const a = placedReactors[first];
        const b = placedReactors[second];
        const vertical = (a.x + a.width === b.x || b.x + b.width === a.x) && a.y < b.y + b.height && a.y + a.height > b.y;
        const horizontal = (a.y + a.height === b.y || b.y + b.height === a.y) && a.x < b.x + b.width && a.x + a.width > b.x;
        if (vertical || horizontal) links += 1;
      }
    }
    assert.equal(links, expectedLinks);
  }
  const demand = calculateEnergy({ targetMw: 400, technology: "fusion", quality: "normal", reservePercent: 0, planet: "nauvis", nuclearWidth: 2, nuclearHeight: 2, fusionMode: "demand", fusionReactors: 2 });
  assert.equal(demand.fusion.blocks, 2); close(demand.fusion.grossMw, 800); close(demand.fusion.netMw, 753.8);
  assert.equal(demand.entities.some((entry) => entry.id === "pump"), false);
  close(demand.inputs.find((entry) => entry.id === "fluoroketone-cold").perSecond, 16);
  const tower = calculateEnergy({ targetMw: 1, technology: "heating-tower", quality: "normal", reservePercent: 0, planet: "nauvis", nuclearWidth: 1, nuclearHeight: 1, fusionMode: "fixed", fusionReactors: 1 });
  assert.match(tower.entities.find((e) => e.id === "heating-tower").detail, /40\.0 MW/);
  close(tower.grossMw, 40);
});

test("power buildings apply their individual quality effects without changing physical ratios", () => {
  const base = { targetMw: 900, reservePercent: 0, planet: "nauvis", nuclearWidth: 2, nuclearHeight: 2, fusionMode: "demand", fusionReactors: 1 };
  const steamNormal = calculateEnergy({ ...base, technology: "steam", quality: "normal" });
  const steamLegendary = calculateEnergy({ ...base, technology: "steam", quality: "legendary" });
  assert.equal(steamNormal.entities.find((e) => e.id === "offshore-pump").amount, 3);
  assert.equal(steamLegendary.entities.find((e) => e.id === "offshore-pump").amount, 1);
  close(steamNormal.inputs.find((entry) => entry.id === "coal").perSecond, 225);
  close(steamLegendary.inputs.find((entry) => entry.id === "coal").perSecond, 225);
  const nuclearNormal = calculateEnergy({ ...base, targetMw: 1, technology: "nuclear", quality: "normal" });
  const nuclearLegendary = calculateEnergy({ ...base, targetMw: 1, technology: "nuclear", quality: "legendary" });
  close(nuclearNormal.grossMw, 480);
  close(nuclearLegendary.grossMw, 1200);
  assert.equal(nuclearNormal.entities.find((e) => e.id === "steam-turbine").amount, nuclearLegendary.entities.find((e) => e.id === "steam-turbine").amount);
  assert.equal(nuclearLegendary.entities.find((e) => e.id === "heat-exchanger").amount, nuclearNormal.entities.find((e) => e.id === "heat-exchanger").amount);
  assert.equal(nuclearNormal.entities.find((e) => e.id === "offshore-pump").amount, 1);
  assert.equal(nuclearLegendary.entities.find((e) => e.id === "offshore-pump").amount, 1);
});

test("energy routes expose finite compact blueprints and nuclear dimensions are meaningful", () => {
  const base = { targetMw: 100, quality: "normal", reservePercent: 0, planet: "nauvis", nuclearWidth: 1, nuclearHeight: 1, fusionMode: "fixed", fusionReactors: 1 };
  for (const technology of ["steam", "solar", "nuclear", "heating-tower", "fusion"]) {
    const result = calculateEnergy({ ...base, technology });
    const legendary = calculateEnergy({ ...base, technology, quality: "legendary" });
    assert.ok(result.blueprint.placements.length > 0, technology);
    assert.ok(legendary.blueprint.placements.length > 0, technology);
    assert.equal(result.blueprint.placements.some((entry) => entry.entityId === "offshore-pump"), false);
    for (const placement of [...result.blueprint.placements, ...legendary.blueprint.placements]) {
      assert.ok([placement.x, placement.y, placement.width, placement.height].every(Number.isFinite));
      assert.equal(placement.direction, undefined, `${technology} entities remain upright`);
    }
  }
  const single = calculateEnergy({ ...base, technology: "nuclear" });
  const pair = calculateEnergy({ ...base, technology: "nuclear", nuclearWidth: 2 });
  assert.equal(single.grossMw, 120);
  assert.equal(pair.grossMw, 160);
  assert.equal(single.blueprint.placements.filter((entry) => entry.entityId === "nuclear-reactor").length, 1);
  assert.equal(pair.blueprint.placements.filter((entry) => entry.entityId === "nuclear-reactor").length, 2);
  const spaceSolar = calculateEnergy({ ...base, technology: "solar", planet: "space" });
  assert.equal(spaceSolar.blueprint.placements.some((entry) => entry.entityId === "accumulator"), false);
});

test("recycler timing, fluid loss and quality returns are expected values", () => {
  const route = RECYCLING_ROUTE_BY_ID.get("advanced-circuit-recycling");
  const normal = calculateRecycling({
    route, amount: 60, unit: "s", inputQuality: "normal", maxQuality: "legendary",
    recyclerQuality: "normal", modules: [null,null,null,null], beaconEnabled: false,
    beaconCount: 0, beaconModules: [null,null], beaconQuality: "normal", qualityMode: false,
  });
  close(route.energy, .375); close(normal.throughputPerRecycler, 4 / 3); close(normal.recyclerTimeSeconds, .75);
  assert.equal(normal.recyclersToBuild, 45);
  const battery = calculateRecycling({
    route: RECYCLING_ROUTE_BY_ID.get("battery-recycling"), amount: 1, unit: "s",
    inputQuality: "normal", maxQuality: "legendary", recyclerQuality: "normal",
    modules: [null,null,null,null], beaconEnabled: false, beaconCount: 0,
    beaconModules: [null,null], beaconQuality: "normal", qualityMode: false,
  });
  assert.equal(battery.lostFluids[0].id, "sulfuric-acid");
  const quality = calculateRecycling({
    route, amount: 1, unit: "s", inputQuality: "normal", maxQuality: "legendary",
    recyclerQuality: "normal",
    modules: Array.from({length:4},()=>({family:"quality",tier:3,quality:"normal"})),
    beaconEnabled: true, beaconCount: 1, beaconModules: [{family:"speed",tier:3,quality:"normal"},null],
    beaconQuality: "normal", qualityMode: true,
  });
  close(quality.qualityChance, .0625);
  assert.ok(quality.outputs[0].amounts.some((entry) => entry.quality === "legendary"));
});

test("recycling selectors expose only actual generated recipes", () => {
  assert.equal(RECYCLING_ROUTES.length, 286);
  assert.equal(RECYCLING_PRODUCTS.length, 286);
  assert.equal(QUALITY_RECYCLING_PRODUCTS.length, 284);
  assert.ok(RECYCLING_ROUTES.every((route) => route.id.endsWith("-recycling")));
  assert.ok(RECYCLING_ROUTES.every((route) => route.outputs.every((output) => Number.isFinite(output.amount))));
  assert.ok(!RECYCLING_PRODUCTS.some((product) => product.id === "item-unknown"));
  assert.ok(!RECYCLING_PRODUCTS.some((product) => product.id === "infinity-chest"));
  assert.ok(RECYCLING_ROUTES.every((route) => route.productIcon !== "/favicon.svg"));
});

test("rocket freight applies item weight, silo productivity and route distance", () => {
  assert.equal(ROCKET_PRODUCT_BY_ID.get("bioflux").capacity, 1000);
  const result = calculateRocketFreight({
    productId: "bioflux", source: "gleba", destination: "nauvis", targetRate: 60, unit: "min",
    cargoPerTransit: 1000, siloQuality: "normal", rocketPartProductivityLevel: 0,
    siloModules: [null,null,null,null], beaconCount: 0, beaconModules: [null,null], beaconQuality: "normal",
    speedMode: "thrusters", manualSpeedKms: 250, thrusterCount: 4, thrusterQuality: "normal",
    thrusterFill: .8, platformMassTons: 1000, platformWidthTiles: 17, cargoBays: 0, includeEnergy: false,
  });
  assert.equal(result.valid, true);
  assert.equal(result.distanceKm, 15000);
  assert.equal(result.launchesPerTransit, 1);
  assert.ok(result.speedKms > 100);
  assert.equal(result.rocketPartIngredientsPerSecond.length, 3);
  assert.ok(result.flightSeconds > 0);
  assert.ok(result.roundTripSeconds > result.flightSeconds * 2);
  assert.equal(result.siloLayout.machineCount, result.waveSilos);
});

test("every launchable item and connected location pair yields finite freight results", () => {
  const base = {
    source: "gleba",
    destination: "nauvis",
    targetRate: 1,
    unit: "min",
    cargoPerTransit: 1,
    siloQuality: "normal",
    rocketPartProductivityLevel: 0,
    siloModules: [null, null, null, null],
    beaconCount: 0,
    beaconModules: [null, null],
    beaconQuality: "normal",
    speedMode: "manual",
    manualSpeedKms: 250,
    thrusterCount: 1,
    thrusterQuality: "normal",
    thrusterFill: 0.8,
    platformMassTons: 1000,
    platformWidthTiles: 5,
    cargoBays: 0,
    includeEnergy: false,
  };
  for (const product of ROCKET_PRODUCTS) {
    const result = calculateRocketFreight({ ...base, productId: product.id });
    assert.equal(result.valid, true, product.id);
    assert.ok(product.capacity > 0);
    assert.ok([result.flightSeconds, result.roundTripSeconds, result.launchesPerMinute, result.electricKw].every(Number.isFinite));
  }
  for (const source of ROCKET_LOCATIONS) {
    for (const destination of ROCKET_LOCATIONS) {
      if (source === destination) continue;
      const result = calculateRocketFreight({ ...base, productId: "bioflux", source, destination });
      assert.equal(result.valid, true, `${source} → ${destination}`);
      assert.ok(result.distanceKm > 0);
      assert.ok(Number.isFinite(result.flightSeconds));
    }
  }
});

test("logistics warnings use per-machine load, not aggregate load", () => {
  const input = setup("advanced-circuit", "item:advanced-circuit", "electromagnetic-plant", { amount: 60 });
  const result = calculate(input);
  const logistics = calculateLogistics(result, input, DEFAULT_LOGISTICS);
  assert.equal(logistics.warnings.length, 0);
  assert.ok(logistics.flows.every((flow) => flow.perMachineMax < 240));
  assert.ok(logistics.flows.filter((flow) => flow.type === "item").every((flow) => flow.totalInserters > 0));
});

test("full chains reach raw pumps or drills and honor node energy flags", () => {
  const input = setup("sulfuric-acid", "fluid:sulfuric-acid", "chemical-plant", { amount: 100 });
  const result = calculate(input);
  const chain = buildProductionChain({ rootInput: input, rootResult: result, maxDepth: 10 });
  assert.ok(chain.rawSources.some((source) => source.id === "water"));
  assert.ok(chain.totalElectricKw >= chain.root.electricKw);
  assert.ok(chain.includedElectricKw > 0);
});

test("blue science defaults to canonical Nauvis routes without Gleba operating inputs", () => {
  const gears = PRODUCT_BY_KEY.get("item:iron-gear-wheel");
  assert.equal(preferredRecipeForProduct(gears).id, "casting-iron-gear-wheel");
  assert.equal(preferredChainRecipeForProduct(gears).id, "iron-gear-wheel");
  const input = setup(
    "chemical-science-pack",
    "item:chemical-science-pack",
    "assembling-machine-3",
    { amount: 60 },
  );
  const chain = buildProductionChain({
    rootInput: input,
    rootResult: calculate(input),
  });
  const nodes = [];
  const visit = (node) => {
    if (node.kind !== "recipe") return;
    nodes.push(node);
    node.inputs.forEach(visit);
  };
  visit(chain.root);
  assert.ok(nodes.some((node) => node.recipe.id === "plastic-bar"));
  assert.ok(nodes.some((node) => node.recipe.id === "advanced-oil-processing"));
  assert.ok(nodes.some((node) => node.recipe.id === "sulfur"));
  assert.equal(
    nodes.some((node) =>
      ["nutrients", "biter-egg", "bioflux"].includes(node.product.id),
    ),
    false,
  );
  assert.equal(
    nodes.some((node) => ["bioplastic", "biosulfur"].includes(node.recipe.id)),
    false,
  );
});

test("chain-wide process choices select an alternate route and machine for every matching node", () => {
  const input = setup(
    "automation-science-pack",
    "item:automation-science-pack",
    "assembling-machine-3",
    { amount: 30, unit: "min" },
  );
  const master = {
    preferredMachineId: "auto",
    processMachineChoices: {
      "item:iron-gear-wheel": {
        recipeId: "casting-iron-gear-wheel",
        machineId: "foundry",
      },
    },
    machineQuality: "normal",
    machineModuleFamilies: [],
    machineModuleTier: 3,
    machineModuleQuality: "normal",
    beaconEnabled: false,
    beaconMode: "standard",
    beaconCount: 0,
    beaconModuleFamily: "speed",
    beaconModuleTier: 3,
    beaconModuleQuality: "normal",
    beaconQuality: "normal",
  };
  const chain = buildProductionChain({ rootInput: input, masterSettings: master });
  const gear = chain.root.inputs.find(
    (node) => node.kind === "recipe" && node.product.id === "iron-gear-wheel",
  );
  assert.ok(gear && gear.kind === "recipe");
  assert.equal(gear.recipe.id, "casting-iron-gear-wheel");
  assert.equal(gear.machine.id, "foundry");
});

test("chain-wide and per-node inserter settings drive upstream logistics", () => {
  const input = setup(
    "automation-science-pack",
    "item:automation-science-pack",
    "assembling-machine-3",
    { amount: 30, unit: "min" },
  );
  const master = {
    preferredMachineId: "auto",
    processMachineChoices: {},
    machineQuality: "normal",
    machineModuleFamilies: [],
    machineModuleTier: 3,
    machineModuleQuality: "normal",
    beaconEnabled: false,
    beaconMode: "standard",
    beaconCount: 0,
    beaconModuleFamily: "speed",
    beaconModuleTier: 3,
    beaconModuleQuality: "normal",
    beaconQuality: "normal",
    inserterId: "fast-inserter",
    inserterQuality: "rare",
    inserterCapacityBonus: 3,
    inserterHandoff: "container-machine",
  };
  const inherited = buildProductionChain({
    rootInput: input,
    masterSettings: master,
    logisticsSettings: DEFAULT_LOGISTICS,
  });
  const gear = inherited.root.inputs.find(
    (node) => node.kind === "recipe" && node.product.id === "iron-gear-wheel",
  );
  assert.ok(gear && gear.kind === "recipe");
  assert.deepEqual(gear.logisticsSettings, {
    inserterId: "fast-inserter",
    inserterQuality: "rare",
    capacityBonus: 3,
    handoff: "container-machine",
  });
  assert.deepEqual(inherited.root.logisticsSettings, DEFAULT_LOGISTICS);

  const overridden = buildProductionChain({
    rootInput: input,
    masterSettings: master,
    logisticsSettings: DEFAULT_LOGISTICS,
    overrides: {
      [gear.key]: {
        inserterId: "bulk-inserter",
        inserterQuality: "legendary",
        inserterCapacityBonus: 8,
        inserterHandoff: "machine-belt",
      },
    },
  });
  const overriddenGear = overridden.root.inputs.find(
    (node) => node.kind === "recipe" && node.product.id === "iron-gear-wheel",
  );
  assert.ok(overriddenGear && overriddenGear.kind === "recipe");
  assert.deepEqual(overriddenGear.logisticsSettings, {
    inserterId: "bulk-inserter",
    inserterQuality: "legendary",
    capacityBonus: 8,
    handoff: "machine-belt",
  });
});

test("chain machine research and per-family priority select only unlocked compatible machines", () => {
  const plastic = RECIPE_BY_ID.get("plastic-bar");
  assert.ok(plastic);
  const base = {
    preferredMachineId: "auto",
    unlockedMachineIds: [...DEFAULT_UNLOCKED_MACHINE_IDS],
    machinePriorities: {
      ...DEFAULT_MACHINE_PRIORITIES,
      fluids: ["chemical-plant", "cryogenic-plant", "oil-refinery"],
    },
  };
  assert.equal(rankedMachinesForRecipe(plastic, base)[0].id, "chemical-plant");
  const withoutChemical = {
    ...base,
    unlockedMachineIds: base.unlockedMachineIds.filter(
      (id) => id !== "chemical-plant",
    ),
  };
  assert.equal(
    rankedMachinesForRecipe(plastic, withoutChemical)[0].id,
    "cryogenic-plant",
  );
});

test("all-production totals merge shared raw resources and preserve machine contributions by chain", () => {
  const redInput = setup(
    "automation-science-pack",
    "item:automation-science-pack",
    "assembling-machine-3",
    { amount: 30, unit: "min" },
  );
  const greenInput = setup(
    "logistic-science-pack",
    "item:logistic-science-pack",
    "assembling-machine-3",
    { amount: 30, unit: "min" },
  );
  const redResult = calculate(redInput);
  const greenResult = calculate(greenInput);
  const redChain = buildProductionChain({ rootInput: redInput, rootResult: redResult });
  const greenChain = buildProductionChain({ rootInput: greenInput, rootResult: greenResult });
  const redLogistics = calculateLogistics(redResult, redInput, DEFAULT_LOGISTICS);
  const greenLogistics = calculateLogistics(greenResult, greenInput, DEFAULT_LOGISTICS);
  const totals = buildFactoryTotals([
    {
      id: "red",
      product: redInput.product,
      input: redInput,
      result: redResult,
      chain: redChain,
      logisticsSettings: DEFAULT_LOGISTICS,
      logisticsResult: redLogistics,
    },
    {
      id: "green",
      product: greenInput.product,
      input: greenInput,
      result: greenResult,
      chain: greenChain,
      logisticsSettings: DEFAULT_LOGISTICS,
      logisticsResult: greenLogistics,
    },
  ]);
  const ironOre = totals.resources.find((resource) => resource.id === "iron-ore");
  assert.ok(ironOre);
  assert.equal(ironOre.contributions.length, 2);
  assert.ok(ironOre.total > 0);
  const ironMaterial = totals.materials.find(
    (material) => material.id === "iron-ore",
  );
  assert.ok(ironMaterial);
  assert.equal(ironMaterial.contributions.length, 2);
  const gearMaterial = totals.materials.find(
    (material) => material.id === "iron-gear-wheel",
  );
  assert.ok(gearMaterial);
  assert.equal(gearMaterial.contributions.length, 2);
  assert.ok(
    totals.materials.some(
      (material) => material.id === "automation-science-pack",
    ),
  );
  const gears = totals.processes.find(
    (process) => process.product.id === "iron-gear-wheel",
  );
  assert.ok(gears);
  assert.equal(gears.contributions.length, 2);
  assert.equal(
    gears.machinesToBuild,
    gears.contributions.reduce(
      (sum, contribution) => sum + (contribution.machinesToBuild ?? 0),
      0,
    ),
  );
  assert.ok(
    gears.contributions.every(
      (contribution) =>
        contribution.inserterLoads?.some(
          (load) =>
            load.inserterId === DEFAULT_LOGISTICS.inserterId &&
            load.inputTotal + load.outputTotal > 0 &&
            load.inputFlows.length > 0 &&
            load.outputFlows.length > 0,
        ),
    ),
  );
  assert.ok(totals.buildItems.some((item) => item.kind === "machine"));
  assert.ok(totals.buildItems.some((item) => item.kind === "inserter"));
});

test("chain miner selection resizes raw extraction and uses a capable fallback", () => {
  const input = setup(
    "automation-science-pack",
    "item:automation-science-pack",
    "assembling-machine-3",
    { amount: 30, unit: "min" },
  );
  const big = buildProductionChain({
    rootInput: input,
    masterSettings: { miningDrillId: "big-mining-drill" },
  });
  const electric = buildProductionChain({
    rootInput: input,
    masterSettings: { miningDrillId: "electric-mining-drill" },
  });
  const bigIron = big.rawSources.find((source) => source.id === "iron-ore");
  const electricIron = electric.rawSources.find(
    (source) => source.id === "iron-ore",
  );
  assert.ok(bigIron && electricIron);
  assert.equal(bigIron.entityId, "big-mining-drill");
  assert.equal(electricIron.entityId, "electric-mining-drill");
  close(electricIron.entities, bigIron.entities * 5);
  const totals = buildFactoryTotals([
    {
      id: "electric-miner",
      product: electric.rootInput.product,
      input: electric.rootInput,
      result: electric.rootResult,
      chain: electric,
    },
  ]);
  assert.ok(
    totals.buildItems.some(
      (item) =>
        item.kind === "source" && item.id === "electric-mining-drill",
    ),
  );
  assert.equal(
    resolveMiningDrill("item:tungsten-ore", "electric-mining-drill").id,
    "big-mining-drill",
  );
});

test("quality mining fills every drill slot and feeds its quality mix into the chain", () => {
  const mining = miningSourceForBaseline(
    "item:iron-ore",
    2.5,
    "big-mining-drill",
    { family: "quality", tier: 3, quality: "normal" },
  );
  assert.equal(mining.drill.moduleSlots, 4);
  assert.equal(mining.modules.length, 4);
  assert.ok(mining.modules.every((module) => module?.family === "quality"));
  close(mining.qualityChance, 0.1);
  close(mining.rate, 2);

  const qualityModule = { family: "quality", tier: 3, quality: "normal" };
  const input = setup("iron-plate", "item:iron-plate", "electric-furnace", {
    amount: 1,
    outputQuality: "rare",
    calculationMode: "quality",
    machineModules: [qualityModule, qualityModule],
  });
  const chain = buildProductionChain({
    rootInput: input,
    rootResult: calculate(input),
    masterSettings: {
      preferredMachineId: "auto",
      miningDrillId: "big-mining-drill",
      miningModuleFamily: "quality",
      miningModuleTier: 3,
      miningModuleQuality: "normal",
      machineQuality: "normal",
      machineModuleFamilies: ["quality"],
      machineModuleTier: 3,
      machineModuleQuality: "normal",
      beaconEnabled: false,
      beaconMode: "quality-safe",
      beaconCount: 0,
      beaconModuleFamily: "none",
      beaconModuleTier: 3,
      beaconModuleQuality: "normal",
      beaconQuality: "normal",
    },
  });
  const ore = chain.rawSources.find((source) => source.id === "iron-ore");
  assert.ok(ore);
  assert.equal(ore.entityId, "big-mining-drill");
  assert.equal(ore.moduleSlots, 4);
  assert.equal(ore.modules.length, 4);
  close(ore.qualityChance, 0.1);
  close(
    ore.outputAmounts.reduce((sum, entry) => sum + entry.amount, 0),
    ore.ratePerSecond,
  );
  assert.deepEqual(
    ore.outputAmounts.map((entry) => entry.quality),
    ["normal", "uncommon", "rare", "epic", "legendary"],
  );
  close(ore.entities, ore.ratePerSecond / 2);

  const totals = buildFactoryTotals([
    {
      id: "quality-mining",
      product: chain.rootInput.product,
      input: chain.rootInput,
      result: chain.rootResult,
      chain,
    },
  ]);
  const minerModules = totals.buildItems.find(
    (item) =>
      item.kind === "module" &&
      item.id === "quality-module-3" &&
      item.quality === "normal",
  );
  assert.ok(minerModules);
  assert.ok(
    minerModules.amount >= Math.ceil(ore.entities - 1e-12) * ore.moduleSlots,
  );
});

test("all-production unit follows the most-used target unit", () => {
  assert.equal(mostUsedRateUnit(["min", "s", "min", "h"]), "min");
  assert.equal(mostUsedRateUnit(["h", "s"]), "h");
  assert.equal(mostUsedRateUnit([], "min"), "min");
});

test("belt throughput uses exact base rates and researched stack sizes", () => {
  assert.equal(beltCapacityPerSecond("transport-belt", 1), 15);
  assert.equal(beltCapacityPerSecond("fast-transport-belt", 2), 60);
  assert.equal(beltCapacityPerSecond("express-transport-belt", 3), 135);
  assert.equal(beltCapacityPerSecond("turbo-transport-belt", 4), 240);

  const partial = calculateBeltLoad(60, "turbo-transport-belt", 4);
  close(partial.exactBelts, 0.25);
  assert.equal(partial.beltsRequired, 1);
  close(partial.lastBeltUtilization, 0.25);

  const overflow = calculateBeltLoad(241, "turbo-transport-belt", 4);
  assert.equal(overflow.beltsRequired, 2);
  close(overflow.lastBeltUtilization, 1 / 240);
});

test("quality-chain modules size upstream recipes for the exact required feedstock quality", () => {
  const input = setup(
    "automation-science-pack",
    "item:automation-science-pack",
    "assembling-machine-3",
    { amount: 1 },
  );
  const rootResult = calculate(input);
  const master = {
    preferredMachineId: "auto",
    machineQuality: "normal",
    machineModuleFamilies: ["quality"],
    machineModuleTier: 3,
    machineModuleQuality: "legendary",
    beaconEnabled: true,
    beaconMode: "quality-safe",
    beaconCount: 12,
    beaconModuleFamily: "speed",
    beaconModuleTier: 3,
    beaconModuleQuality: "normal",
    beaconQuality: "normal",
  };
  const chain = buildProductionChain({ rootInput: input, rootResult, masterSettings: master });
  const gear = chain.root.inputs.find(
    (node) => node.kind === "recipe" && node.product.id === "iron-gear-wheel",
  );
  assert.ok(gear && gear.kind === "recipe");
  assert.equal(gear.requiredQuality, "normal");
  close(gear.result.ratePerSecond, gear.ratePerSecond);
  const gearOutput = gear.result.outputs.find((flow) => flow.id === "iron-gear-wheel");
  const normalOutput = gearOutput.amounts.find((entry) => entry.quality === "normal").amount;
  close(normalOutput, gear.ratePerSecond);
  assert.ok(gearOutput.total > gear.ratePerSecond);
  assert.equal(gear.result.beaconLayout.requestedPerMachine, 1);
  assert.ok(gear.result.qualityChance > 0);
});

test("quality-chain output qualities feed back into every upstream consumer and the root target", () => {
  const qualityModule = {
    family: "quality",
    tier: 3,
    quality: "legendary",
  };
  const input = setup(
    "automation-science-pack",
    "item:automation-science-pack",
    "assembling-machine-3",
    {
      amount: 1,
      outputQuality: "legendary",
      calculationMode: "quality",
      machineModules: Array.from({ length: 4 }, () => qualityModule),
    },
  );
  const baseResult = calculate(input);
  const master = {
    preferredMachineId: "auto",
    machineQuality: "normal",
    machineModuleFamilies: ["quality"],
    machineModuleTier: 3,
    machineModuleQuality: "legendary",
    beaconEnabled: false,
    beaconMode: "quality-safe",
    beaconCount: 0,
    beaconModuleFamily: "speed",
    beaconModuleTier: 3,
    beaconModuleQuality: "normal",
    beaconQuality: "normal",
  };
  const rootKey = "root:item:automation-science-pack";
  const chain = buildProductionChain({
    rootInput: input,
    rootResult: baseResult,
    masterSettings: master,
    overrides: {
      [`${rootKey}/0:item:copper-plate`]: { recipeId: "copper-plate" },
      [`${rootKey}/1:item:iron-gear-wheel`]: { recipeId: "iron-gear-wheel" },
      [`${rootKey}/1:item:iron-gear-wheel/0:item:iron-plate`]: {
        recipeId: "iron-plate",
      },
    },
  });
  assert.equal(chain.qualityCoupled, true);
  assert.ok(chain.rootInput.inputQualityDistribution.length > 1);
  assert.ok(chain.rootResult.cyclesPerSecond < baseResult.cyclesPerSecond);
  const legendaryOutput = chain.rootResult.outputs
    .find((flow) => flow.id === "automation-science-pack")
    .amounts.find((entry) => entry.quality === "legendary").amount;
  close(legendaryOutput, 1);
  assert.ok(
    chain.rootResult.inputs.every(
      (flow) => flow.type === "fluid" || flow.amounts.length > 1,
    ),
  );

  const gear = chain.root.inputs.find(
    (node) => node.kind === "recipe" && node.product.id === "iron-gear-wheel",
  );
  assert.ok(gear && gear.kind === "recipe");
  const ironPlateDemand = gear.result.inputs.find(
    (flow) => flow.id === "iron-plate",
  );
  assert.ok(ironPlateDemand.amounts.length > 1);
  const plate = gear.inputs.find(
    (node) => node.kind === "recipe" && node.product.id === "iron-plate",
  );
  assert.ok(plate && plate.kind === "recipe");
  const plateOutput = plate.result.outputs.find(
    (flow) => flow.id === "iron-plate",
  );
  for (const required of ironPlateDemand.amounts) {
    const produced =
      plateOutput.amounts.find((entry) => entry.quality === required.quality)
        ?.amount ?? 0;
    assert.ok(produced + 1e-9 >= required.amount);
  }
});

test("manual Biolab 16 uses the same optimized, fully effective layout", () => {
  const common = {
    labId: "biolab", labQuality: "normal", packQuality: "normal", mode: "effective",
    amountPerMinute: 600, machineModules: [null,null,null,null], beaconEnabled: true,
    beaconModules: [null,null], beaconQuality: "normal",
  };
  const manual = calculateScience({ ...common, beaconCount: 16, biolabBeaconMode: "manual" });
  const optimized = calculateScience({ ...common, beaconCount: 1, biolabBeaconMode: "optimized" });
  assert.deepEqual(manual.beaconLayout.counts, optimized.beaconLayout.counts);
  assert.deepEqual(manual.beaconLayout.placements, optimized.beaconLayout.placements);
  assert.ok(
    manual.beaconLayout.placements
      .filter((entry) => entry.kind === "beacon")
      .every((entry) => entry.affects?.length),
  );
});

test("energy blueprints use selected ratios and keep every entity upright", () => {
  const base = {
    targetMw: 1, quality: "normal", reservePercent: 0, planet: "nauvis",
    nuclearWidth: 1, nuclearHeight: 1, fusionMode: "fixed", fusionReactors: 1,
  };
  const boilerEngines = calculateEnergy({ ...base, technology: "steam", steamGenerator: "steam-engine" });
  const boilerTurbine = calculateEnergy({ ...base, technology: "steam", steamGenerator: "steam-turbine" });
  assert.equal(boilerEngines.entities.find((entry) => entry.id === "steam-engine").amount, 2);
  assert.equal(boilerEngines.entities.find((entry) => entry.id === "boiler").amount, 1);
  assert.equal(boilerTurbine.entities.find((entry) => entry.id === "steam-turbine").amount, 1);
  assert.equal(boilerTurbine.entities.find((entry) => entry.id === "boiler").amount, 1);

  const nuclearOne = calculateEnergy({ ...base, technology: "nuclear", steamGenerator: "steam-turbine" });
  const nuclearFour = calculateEnergy({ ...base, technology: "nuclear", nuclearWidth: 2, nuclearHeight: 2, steamGenerator: "steam-turbine" });
  assert.equal(nuclearOne.blueprint.placements.filter((entry) => entry.entityId === "heat-exchanger").length, 4);
  assert.equal(nuclearFour.blueprint.placements.filter((entry) => entry.entityId === "heat-exchanger").length, 48);
  assert.equal(nuclearOne.blueprint.placements.filter((entry) => entry.entityId === "steam-turbine").length, 7);
  assert.equal(nuclearFour.blueprint.placements.filter((entry) => entry.entityId === "steam-turbine").length, 83);
  assert.equal(nuclearFour.blueprint.placements.some((entry) => entry.kind === "link"), false);
  const wasteful = calculateEnergy({ ...base, technology: "nuclear", steamGenerator: "steam-engine" });
  assert.ok(wasteful.netMw < nuclearOne.netMw);

  const nauvisSolar = calculateEnergy({ ...base, technology: "solar" });
  const vulcanusSolar = calculateEnergy({ ...base, technology: "solar", planet: "vulcanus" });
  const legendarySolar = calculateEnergy({ ...base, technology: "solar", quality: "legendary" });
  assert.equal(nauvisSolar.entities.find((entry) => entry.id === "solar-panel").amount, 24);
  assert.equal(nauvisSolar.entities.find((entry) => entry.id === "accumulator").amount, 21);
  assert.equal(vulcanusSolar.entities.find((entry) => entry.id === "accumulator").amount, 5);
  assert.equal(legendarySolar.entities.find((entry) => entry.id === "accumulator").amount, 4);

  const fusion = calculateEnergy({ ...base, targetMw: 0, technology: "fusion", fusionReactors: 4 });
  const fusionGenerators = fusion.blueprint.placements.filter((entry) => entry.entityId === "fusion-generator");
  assert.equal(fusionGenerators.length, 28);
  assert.ok(fusion.blueprint.placements.every((entry) => entry.direction === undefined));
  assert.equal(fusion.blueprint.placements.some((entry) => entry.kind === "link"), false);
  assert.equal(fusion.blueprint.placements.filter((entry) => entry.entityId === "cryogenic-plant").length, 4);
});

test("quality recycling sizes from the selected target-quality output", () => {
  const route = RECYCLING_ROUTE_BY_ID.get("advanced-circuit-recycling");
  const base = {
    route, amount: 1, unit: "s", inputQuality: "normal", maxQuality: "legendary",
    recyclerQuality: "normal",
    modules: Array.from({length:4},()=>({family:"quality",tier:3,quality:"legendary"})),
    beaconEnabled: true, beaconCount: 1,
    beaconModules: [{family:"speed",tier:3,quality:"normal"}],
    beaconQuality: "normal", qualityMode: true,
    targetOutputId: "electronic-circuit", amountMode: "target-output",
  };
  const legendary = calculateRecycling({ ...base, targetQuality: "legendary" });
  const uncommon = calculateRecycling({ ...base, targetQuality: "uncommon" });
  close(legendary.targetOutputPerSecond, 1);
  close(uncommon.targetOutputPerSecond, 1);
  assert.ok(legendary.inputPerSecond > uncommon.inputPerSecond);
  assert.ok(legendary.recyclersToBuild > uncommon.recyclersToBuild);
});

test("every official Quality Recycling route preserves loss and quality expectations", () => {
  const qualityModules = Array.from({ length: 4 }, () => ({
    family: "quality",
    tier: 3,
    quality: "legendary",
  }));
  for (const route of RECYCLING_ROUTES) {
    const normal = calculateRecycling({
      route,
      amount: 1,
      unit: "s",
      inputQuality: "rare",
      maxQuality: "legendary",
      recyclerQuality: "normal",
      modules: [null, null, null, null],
      beaconEnabled: false,
      beaconCount: 0,
      beaconModules: [null, null],
      beaconQuality: "normal",
      qualityMode: false,
      targetOutputId: route.outputs[0].id,
      amountMode: "input",
    });
    for (const expected of route.outputs) {
      const output = normal.outputs.find((flow) => flow.id === expected.id);
      close(output.total, expected.amount);
      assert.deepEqual(output.amounts, [
        { quality: "rare", amount: expected.amount },
      ]);
    }
    for (const expected of route.lostFluids) {
      close(
        normal.lostFluids.find((flow) => flow.id === expected.id).total,
        expected.amount,
      );
    }

    if (!route.qualityEligible) continue;
    for (const expected of route.outputs) {
      const quality = calculateRecycling({
        route,
        amount: 1,
        unit: "s",
        inputQuality: "normal",
        maxQuality: "legendary",
        recyclerQuality: "normal",
        modules: qualityModules,
        beaconEnabled: false,
        beaconCount: 0,
        beaconModules: [null, null],
        beaconQuality: "normal",
        qualityMode: true,
        targetOutputId: expected.id,
        targetQuality: "legendary",
        amountMode: "target-output",
      });
      close(quality.targetOutputPerSecond, 1);
      const output = quality.outputs.find((flow) => flow.id === expected.id);
      close(output.amounts.reduce((sum, entry) => sum + entry.amount, 0), output.total);
      assert.ok(output.amounts.every((entry) => entry.quality !== null));
    }
  }

  const advanced = RECYCLING_ROUTE_BY_ID.get("advanced-circuit-recycling");
  assert.deepEqual(
    Object.fromEntries(advanced.outputs.map((output) => [output.id, output.amount])),
    { "electronic-circuit": 0.5, "plastic-bar": 0.5, "copper-cable": 1 },
  );
  const battery = RECYCLING_ROUTE_BY_ID.get("battery-recycling");
  assert.deepEqual(
    Object.fromEntries(battery.outputs.map((output) => [output.id, output.amount])),
    { "iron-plate": 0.25, "copper-plate": 0.25 },
  );
  assert.deepEqual(
    Object.fromEntries(battery.lostFluids.map((flow) => [flow.id, flow.amount])),
    { "sulfuric-acid": 20 },
  );
});

test("rocket freight updates every silo and reserves the inserter beacon tile", () => {
  const base = {
    productId: "bioflux", source: "gleba", destination: "nauvis", targetRate: 60, unit: "min",
    cargoPerTransit: 3000, siloQuality: "normal", rocketPartProductivityLevel: 0,
    siloModules: [null,null,null,null], beaconCount: 16, beaconModules: [null,null], beaconQuality: "normal",
    speedMode: "manual", manualSpeedKms: 250, thrusterCount: 4, thrusterQuality: "normal",
    thrusterFill: .8, platformMassTons: 1000, platformWidthTiles: 17, cargoBays: 0, includeEnergy: false,
  };
  const robots = calculateRocketFreight({ ...base, loadingMode: "robots" });
  const inserters = calculateRocketFreight({
    ...base, loadingMode: "inserters", loadingInserterId: "stack-inserter",
    loadingInserterQuality: "normal", loadingInserterCapacityBonus: 7,
  });
  const ignored = calculateRocketFreight({ ...base, loadingMode: "ignore" });
  assert.equal(robots.silosToPlace, robots.waveSilos);
  assert.equal(inserters.silosToPlace, inserters.waveSilos);
  assert.ok(inserters.silosToPlace >= 3);
  assert.equal(robots.siloLayout.counts.every((count) => count === 15), true);
  assert.ok(robots.siloLayout.uniqueBeacons < robots.silosToPlace * 16);
  assert.ok(
    robots.siloLayout.placements.some(
      (entry) => entry.kind === "beacon" && (entry.affects?.length ?? 0) > 1,
    ),
  );
  assert.equal(inserters.siloLayout.counts.every((count) => count === 15), true);
  assert.equal(
    inserters.siloLayout.placements.filter((entry) => entry.entityId === "stack-inserter").length,
    inserters.silosToPlace,
  );
  assert.equal(
    robots.siloLayout.placements.filter((entry) => entry.entityId === "buffer-chest").length,
    robots.silosToPlace,
  );
  assert.equal(
    robots.siloLayout.placements.filter((entry) => entry.entityId === "roboport").length,
    robots.silosToPlace,
  );
  assert.equal(ignored.siloLayout.counts.every((count) => count === 16), true);
  assert.equal(ignored.siloLayout.placements.some((entry) => entry.kind === "entity"), false);
  for (const machine of inserters.siloLayout.placements.filter((entry) => entry.kind === "machine")) {
    assert.equal(
      inserters.siloLayout.placements.some(
        (entry) =>
          entry.kind === "beacon" &&
          entry.x === machine.x + 3 &&
          entry.y === machine.y + 9,
      ),
      false,
    );
  }
  assert.ok(robots.payloadInsertionSeconds > 0);
  assert.ok(robots.robotSupplySeconds > 0);
  assert.ok(inserters.payloadInsertionSeconds > 0);
  assert.ok(inserters.siloCycleSeconds > ignored.siloCycleSeconds);
  assert.ok(robots.siloCycleSeconds > ignored.siloCycleSeconds);

  const preview = calculateRocketSiloBeaconDesign(
    MACHINE_BY_ID.get("rocket-silo"),
    4,
    16,
    { mode: "inserters", inserterId: "stack-inserter" },
  );
  assert.equal(preview.machineCount, 4);
  assert.equal(preview.placements.filter((entry) => entry.kind === "machine").length, 4);
  assert.equal(preview.placements.filter((entry) => entry.entityId === "stack-inserter").length, 4);
  const previewMachines = preview.placements.filter((entry) => entry.kind === "machine");
  const leftEdge = Math.min(...previewMachines.map((entry) => entry.x));
  const rightEdge = Math.max(...previewMachines.map((entry) => entry.x + entry.width));
  assert.equal(
    preview.placements
      .filter((entry) => entry.kind === "beacon")
      .every((entry) => entry.x >= leftEdge - entry.width && entry.x <= rightEdge),
    true,
    "multi-silo layouts must not leave detached beacons beyond either end",
  );
});

test("Cargo bay quality scales platform storage and robot research scales local supply", () => {
  const base = {
    productId: "bioflux", source: "gleba", destination: "nauvis", targetRate: 60, unit: "min",
    cargoPerTransit: 10000, siloQuality: "normal", rocketPartProductivityLevel: 0,
    siloModules: [null,null,null,null], beaconCount: 16, beaconModules: [null,null], beaconQuality: "normal",
    speedMode: "manual", manualSpeedKms: 250, thrusterCount: 4, thrusterQuality: "normal",
    thrusterFill: .8, platformMassTons: 1000, platformWidthTiles: 17, cargoBays: 1,
    loadingMode: "robots", loadingInserterId: "stack-inserter", loadingInserterQuality: "normal",
    loadingInserterCapacityBonus: 7, workerRobotSpeedLevel: 0, workerRobotCargoLevel: 0,
    includeEnergy: false,
  };
  const normal = calculateRocketFreight({ ...base, cargoBayQuality: "normal", logisticRobotCount: 1 });
  const legendary = calculateRocketFreight({
    ...base, cargoBayQuality: "legendary", logisticRobotCount: 999,
    workerRobotSpeedLevel: 6, workerRobotCargoLevel: 3,
  });
  assert.equal(normal.cargoBaySlotsEach, 20);
  assert.equal(legendary.cargoBaySlotsEach, 50);
  assert.equal(normal.platformCargoCapacity, 7900);
  assert.equal(legendary.platformCargoCapacity, 10900);
  assert.equal(legendary.cargoStorageLimited, false);
  assert.equal(legendary.logisticRobotCount, 300);
  assert.equal(legendary.robotCargoCapacity, 4);
  assert.ok(legendary.robotSupplySeconds < normal.robotSupplySeconds);
});

test("rocket freight reports when the preloaded payload wave hides silo speed upgrades", () => {
  const result = calculateRocketFreight({
    productId: "bioflux", source: "gleba", destination: "nauvis",
    targetRate: 1, unit: "min", cargoPerTransit: 5000,
    siloQuality: "legendary", rocketPartProductivityLevel: 20,
    siloModules: Array.from({ length: 4 }, () => ({ family: "speed", tier: 3, quality: "legendary" })),
    beaconCount: 16,
    beaconModules: Array.from({ length: 2 }, () => ({ family: "speed", tier: 3, quality: "legendary" })),
    beaconQuality: "legendary", speedMode: "manual", manualSpeedKms: 250,
    thrusterCount: 4, thrusterQuality: "normal", thrusterFill: .8,
    platformMassTons: 1000, platformWidthTiles: 17, cargoBays: 0,
    includeEnergy: false,
  });
  assert.equal(result.siloBottleneck, "payload-wave");
  assert.equal(result.silosToPlace, result.payloadWaveSilos);
  assert.ok(result.payloadWaveSilos >= result.speedLimitedSilos);
  assert.ok(result.notes.some((note) => note.includes("payload-wave")));
});

test("rocket freight reports a platform Cargo bay storage bottleneck", () => {
  const result = calculateRocketFreight({
    productId: "bioflux", source: "gleba", destination: "nauvis",
    targetRate: 60, unit: "min", cargoPerTransit: 10000,
    siloQuality: "normal", rocketPartProductivityLevel: 0,
    siloModules: [null, null, null, null], beaconCount: 0,
    beaconModules: [null, null], beaconQuality: "normal",
    speedMode: "manual", manualSpeedKms: 250, thrusterCount: 4,
    thrusterQuality: "normal", thrusterFill: .8, platformMassTons: 1000,
    platformWidthTiles: 17, cargoBays: 0, includeEnergy: false,
  });
  assert.equal(result.cargoStorageLimited, true);
  assert.equal(result.platformCargoCapacity, 5900);
  assert.equal(result.effectiveCargoPerTransit, 5900);
  assert.equal(result.minimumCargoBays, 3);
  assert.ok(result.notes.some((note) => note.includes("Platform storage")));
});

test("production-chain master settings apply route preference and module fallback", () => {
  const input = setup(
    "production-science-pack",
    "item:production-science-pack",
    "assembling-machine-3",
    { amount: 1 },
  );
  const rootResult = calculate(input);
  const chain = buildProductionChain({
    rootInput: input,
    rootResult,
    maxDepth: 8,
    masterSettings: {
      preferredMachineId: "cryogenic-plant",
      machineQuality: "legendary",
      machineModuleFamilies: ["productivity", "speed"],
      machineModuleTier: 3,
      machineModuleQuality: "normal",
      beaconEnabled: true,
      beaconMode: "standard",
      beaconCount: 4,
      beaconModuleFamily: "speed",
      beaconModuleTier: 3,
      beaconModuleQuality: "normal",
      beaconQuality: "rare",
    },
  });
  const nodes = [];
  const visit = (node) => {
    if (node.kind !== "recipe") return;
    nodes.push(node);
    node.inputs.forEach(visit);
  };
  chain.root.inputs.forEach(visit);
  const plastic = nodes.find((node) => node.product.id === "plastic-bar");
  const furnace = nodes.find((node) => node.product.id === "electric-furnace");
  assert.equal(plastic.machine.id, "cryogenic-plant");
  assert.equal(plastic.result.machineEffects.productivity > 0, true);
  assert.equal(furnace.machine.id, "assembling-machine-3");
  assert.equal(furnace.result.machineEffects.productivity, 0);
  assert.equal(furnace.result.machineEffects.speed > 0, true);
  assert.equal(furnace.result.beaconElectricKw > 0, true);
});

test("downstream navigation exposes only recipes consuming the current material", () => {
  const next = downstreamProducts("item:advanced-circuit");
  assert.ok(next.some((product) => product.id === "processing-unit"));
  for (const product of next) {
    assert.ok(
      product.recipeIds.some((recipeId) =>
        RECIPE_BY_ID.get(recipeId)?.ingredients.some(
          (ingredient) =>
            `${ingredient.type}:${ingredient.name}` === "item:advanced-circuit",
        ),
      ),
    );
  }
  const gears = downstreamProducts("item:iron-gear-wheel");
  assert.ok(
    gears.some((product) => product.id === "automation-science-pack"),
    "clicking the gear output must offer Automation science pack",
  );
});

test("beacon geometry cache reuses results across repeated renders", () => {
  const machine = MACHINE_BY_ID.get("assembling-machine-3");
  const first = calculateBeaconDesign(machine, 36, 12);
  const second = calculateBeaconDesign(machine, 36, 12);
  assert.equal(first, second);
});
