import assert from "node:assert/strict";
import { inflateSync } from "node:zlib";
import test from "node:test";

import { calculate } from "../../app/lib/calculator.ts";
import {
  MACHINE_BY_ID,
  PRODUCT_BY_KEY,
  RECIPE_BY_ID,
} from "../../app/lib/factorio-data.ts";
import {
  calculateLogistics,
  DEFAULT_LOGISTICS,
} from "../../app/lib/logistics.ts";
import { DEFAULT_BLUEPRINT_BLOCK_SETTINGS } from "../../app/lib/blueprint-contract.ts";
import {
  encodeBlueprintString,
  generateBlueprintPlan,
} from "./generator.ts";

function oilNode() {
  const recipe = RECIPE_BY_ID.get("advanced-oil-processing");
  const product = PRODUCT_BY_KEY.get("fluid:petroleum-gas");
  const machine = MACHINE_BY_ID.get("oil-refinery");
  assert.ok(recipe && product && machine);
  const input = {
    recipe,
    product,
    machine,
    machineQuality: "normal",
    inputQuality: "normal",
    outputQuality: "total",
    maxQuality: "legendary",
    calculationMode: "normal",
    machineModules: [null, null, null],
    beaconModules: [
      { family: "speed", tier: 3, quality: "normal" },
      { family: "speed", tier: 3, quality: "normal" },
    ],
    beaconQuality: "normal",
    beaconCount: 8,
    mode: "output",
    amount: 550,
    unit: "s",
    inputConstraints: [],
  };
  const result = calculate(input);
  const logistics = calculateLogistics(result, input, DEFAULT_LOGISTICS);
  return {
    key: "root",
    depth: 0,
    input,
    result,
    logistics,
    logisticsSettings: DEFAULT_LOGISTICS,
  };
}

test("advanced oil keeps two inputs and three outputs on separated pipe lanes", () => {
  const plan = generateBlueprintPlan(
    oilNode(),
    {
      ...DEFAULT_BLUEPRINT_BLOCK_SETTINGS,
      substationPriority: true,
      roboportCoverage: true,
    },
    "en",
  );
  assert.equal(
    plan.fluidRoutes.filter((route) => route.direction === "input").length,
    2,
  );
  assert.equal(
    plan.fluidRoutes.filter((route) => route.direction === "output").length,
    3,
  );
  for (const direction of ["input", "output"]) {
    const routes = plan.fluidRoutes.filter(
      (route) => route.direction === direction,
    );
    for (let first = 0; first < routes.length; first += 1) {
      for (let second = first + 1; second < routes.length; second += 1) {
        assert.ok(Math.abs(routes[first].lane - routes[second].lane) >= 2);
        for (const left of routes[first].ports) {
          for (const right of routes[second].ports) {
            assert.ok(
              Math.abs(left.x - right.x) + Math.abs(left.y - right.y) > 1,
              `${routes[first].id} touches ${routes[second].id}`,
            );
          }
        }
      }
    }
  }
  assert.equal(
    plan.warnings.some((warning) => warning.code === "route-obstacle"),
    false,
  );
  assert.equal(plan.beaconCoverageComplete, true);

  const entities = plan.blueprint.blueprint.entities;
  const refineries = entities.filter((entity) => entity.name === "oil-refinery");
  assert.equal(refineries.length, plan.machineCountInBlock);
  assert.ok(refineries.every((entity) => entity.direction === 0));
  const inputRoutes = plan.fluidRoutes.filter(
    (route) => route.direction === "input",
  );
  const outputRoutes = plan.fluidRoutes.filter(
    (route) => route.direction === "output",
  );
  for (const route of plan.fluidRoutes) {
    assert.equal(route.ports.length, refineries.length);
  }
  for (let index = 0; index < refineries.length; index += 1) {
    const refinery = refineries[index];
    const left = refinery.position.x - 2.5;
    const top = refinery.position.y - 2.5;
    const mirror = (offset) => refinery.mirror ? 4 - offset : offset;
    assert.deepEqual(inputRoutes.map((route) => route.ports[index]), [
      { x: left + mirror(1), y: top + 5 },
      { x: left + mirror(3), y: top + 5 },
    ]);
    assert.deepEqual(outputRoutes.map((route) => route.ports[index]), [
      { x: left + mirror(0), y: top - 1 },
      { x: left + mirror(2), y: top - 1 },
      { x: left + mirror(4), y: top - 1 },
    ]);
  }

  assert.ok(entities.some((entity) => entity.name === "pipe-to-ground" && entity.direction === 4));
  assert.ok(entities.some((entity) => entity.name === "pipe-to-ground" && entity.direction === 12));
  assert.ok(refineries.some((entity) => entity.mirror === true));

});

test("Substations use quality reach and Roboports stay vertically aligned", () => {
  const node = oilNode();
  const normal = generateBlueprintPlan(
    node,
    {
      ...DEFAULT_BLUEPRINT_BLOCK_SETTINGS,
      substationPriority: true,
      substationQuality: "normal",
      roboportCoverage: true,
      roboportSide: "north",
    },
    "en",
  );
  const legendary = generateBlueprintPlan(
    node,
    {
      ...DEFAULT_BLUEPRINT_BLOCK_SETTINGS,
      substationPriority: true,
      substationQuality: "legendary",
      roboportCoverage: true,
      roboportSide: "south",
    },
    "en",
  );
  assert.equal(normal.grid.x % 18, 0);
  assert.equal(normal.grid.y % 18, 0);
  assert.equal(legendary.grid.x % 28, 0);
  assert.equal(legendary.grid.y % 28, 0);
  assert.equal(normal.roboportCoverageComplete, true);
  assert.equal(legendary.roboportCoverageComplete, true);
  for (const plan of [normal, legendary]) {
    const entities = plan.blueprint.blueprint.entities;
    const substations = entities.filter((entity) => entity.name === "substation");
    const roboports = entities.filter((entity) => entity.name === "roboport");
    assert.ok(substations.length >= 4 && roboports.length > 0);
    assert.ok(new Set(substations.map((entity) => entity.position.x)).size >= 2);
    assert.ok(new Set(substations.map((entity) => entity.position.y)).size >= 2);
    assert.equal(plan.substationCoverageComplete, true);
    assert.equal(plan.beaconCoverageComplete, true);
    const reach = plan === normal ? 18 : 28;
    const xs = [...new Set(substations.map((p) => p.position.x))].sort((a, b) => a - b);
    const ys = [...new Set(substations.map((p) => p.position.y))].sort((a, b) => a - b);
    for (const axis of [xs, ys]) {
      for (let i = 1; i < axis.length; i++) assert.equal(axis[i] - axis[i - 1], reach);
    }
    assert.equal(plan.grid.x, xs.at(-1) - xs[0]);
    assert.equal(plan.grid.y, ys.at(-1) - ys[0]);
    for (const roboport of roboports) {
      assert.ok(
        substations.some(
          (substation) => substation.position.x === roboport.position.x,
        ),
      );
    }
  }
});

test("snap-to-grid can be switched off and changes regenerate the blueprint JSON", () => {
  const node = oilNode();
  const snapped = generateBlueprintPlan(
    node,
    { ...DEFAULT_BLUEPRINT_BLOCK_SETTINGS, snapToGrid: true },
    "en",
  );
  const free = generateBlueprintPlan(
    node,
    { ...DEFAULT_BLUEPRINT_BLOCK_SETTINGS, snapToGrid: false },
    "en",
  );
  assert.deepEqual(snapped.blueprint.blueprint["snap-to-grid"], snapped.grid);
  assert.equal("snap-to-grid" in free.blueprint.blueprint, false);
  assert.equal("position-relative-to-grid" in free.blueprint.blueprint, false);
  assert.equal("absolute-snapping" in free.blueprint.blueprint, false);
  assert.notEqual(snapped.blueprintJson, free.blueprintJson);

  const powered = generateBlueprintPlan(
    node,
    {
      ...DEFAULT_BLUEPRINT_BLOCK_SETTINGS,
      snapToGrid: true,
      substationPriority: true,
      substationQuality: "legendary",
    },
    "en",
  );
  assert.notEqual(snapped.blueprintJson, powered.blueprintJson);
  assert.notDeepEqual(snapped.grid, powered.grid);
});

test("overloaded selected belts are marked and upgraded in the generated entities", () => {
  const node = oilNode();
  const output = {
    id: "test-item",
    name: "Test item",
    icon: "",
    type: "item",
    direction: "output",
    totalPerSecond: 100,
    perMachineMin: 100,
    perMachineMax: 100,
    insertersPerMachine: 20,
    totalInserters: 20,
  };
  const plan = generateBlueprintPlan(
    {
      ...node,
      result: {
        ...node.result,
        machinesToBuild: 1,
        beaconLayout: {
          ...node.result.beaconLayout,
          machineCount: 1,
          counts: [0],
          placements: [
            {
              kind: "machine",
              x: 0,
              y: 0,
              width: 5,
              height: 5,
              machineIndex: 0,
            },
          ],
        },
      },
      logistics: { flows: [output], electricKw: 0, warnings: [] },
    },
    DEFAULT_BLUEPRINT_BLOCK_SETTINGS,
    "en",
  );
  assert.equal(plan.requestedBeltOverloaded, true);
  assert.equal(plan.beltLoads[0].load.effectiveBeltId, "turbo-transport-belt");
  assert.ok(plan.beltLoads[0].load.beltsRequired >= 2);
  assert.ok(
    plan.warnings.some((warning) => warning.code === "inserter-output-space"),
  );
  assert.ok(
    plan.blueprint.blueprint.entities.some(
      (entity) => entity.name === "turbo-transport-belt",
    ),
  );
});

test("compressed Blueprint string round-trips to the generated JSON", async () => {
  const plan = generateBlueprintPlan(
    oilNode(),
    DEFAULT_BLUEPRINT_BLOCK_SETTINGS,
    "en",
  );
  const encoded = await encodeBlueprintString(plan.blueprintJson);
  assert.equal(encoded[0], "0");
  const decoded = inflateSync(Buffer.from(encoded.slice(1), "base64")).toString(
    "utf8",
  );
  assert.deepEqual(JSON.parse(decoded), plan.blueprint);
});
