import { readFileSync, statSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";

import {
  analyzeIniRuntimeBoundaries,
  createLinuxCncInterpSdk,
  planIniFileContextStaging,
  planSimConfigStaging,
} from "../../../runtime/sdk/src/index.js";

function verifyExpectedOutput(fixtureName, output, expectedText) {
  const expectedLines = expectedText.split("\n").filter(Boolean);

  for (const expectedLine of expectedLines) {
    if (expectedLine.startsWith("absent=")) {
      const forbidden = expectedLine.slice("absent=".length);
      assert.equal(
        output.includes(forbidden),
        false,
        `${fixtureName}: unexpected ${forbidden}`,
      );
      continue;
    }

    assert.equal(
      output.includes(expectedLine),
      true,
      `${fixtureName}: ${expectedLine}`,
    );
  }
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "build/wasm/core/linuxcnc_interp.wasm");
const vendorRoot = resolve(rootDir, "vendor/linuxcnc");
const manifestText = readFileSync(resolve(rootDir, "tools/source-manifest.txt"), "utf8");

const interp = await createLinuxCncInterpSdk({
  wasmBinary: readFileSync(wasmPath),
  print() {},
  printErr(message) {
    console.error(message);
  },
});

function readSimIni(machineRel, iniFile) {
  return readFileSync(resolve(vendorRoot, `configs/sim/${machineRel}/${iniFile}`), "utf8");
}

const syntheticPlan = planSimConfigStaging({
  manifestText: [
    "configs/sim/example/test.ini",
    "configs/sim/example/test.ngc",
    "configs/sim/example/tool.tbl",
    "configs/sim/example/linuxcnc.var",
    "configs/sim/example/subs/helper.ngc",
    "configs/sim/example/M123",
  ].join("\n"),
  machineRel: "example",
  iniFile: "test.ini",
  iniText: [
    "[DISPLAY]",
    "OPEN_FILE = test.ngc",
    "[RS274NGC]",
    "PARAMETER_FILE = linuxcnc.var",
    "SUBROUTINE_PATH = subs",
    "USER_M_PATH = .",
    "[EMCIO]",
    "TOOL_TABLE = tool.tbl",
  ].join("\n"),
});
assert.deepEqual(
  syntheticPlan.files.map((file) => [file.sourceRel, file.executable]).sort(),
  [
    ["configs/sim/example/M123", true],
    ["configs/sim/example/linuxcnc.var", false],
    ["configs/sim/example/subs/helper.ngc", false],
    ["configs/sim/example/test.ini", false],
    ["configs/sim/example/test.ngc", false],
    ["configs/sim/example/tool.tbl", false],
  ],
  "synthetic sim-config staging plan",
);

const syntheticBoundary = analyzeIniRuntimeBoundaries({
  manifestText: [
    "configs/sim/example/test.ini",
    "configs/sim/example/test.ngc",
    "configs/sim/example/tool.tbl",
    "configs/sim/example/M123",
  ].join("\n"),
  sourceRootRel: "configs/sim/example",
  sourceSearchRootRel: "configs/sim",
  iniFile: "test.ini",
  iniText: [
    "[DISPLAY]",
    "DISPLAY = axis",
    "OPEN_FILE = test.ngc",
    "[RS274NGC]",
    "USER_M_PATH = .",
    "[EMCIO]",
    "TOOL_TABLE = tool.tbl",
  ].join("\n"),
});
assert.deepEqual(
  syntheticBoundary,
  {
    dependencies: [],
    recommendedBlockedKind: "-",
    toolDatabaseProgram: "",
    halRuntime: { values: [], requiresProcess: false },
    uiRuntime: { values: ["axis"], requiresProcess: false },
    haluiRuntime: { mdiCommands: [], requiresProcess: false },
    userMRuntime: {
      paths: ["."],
      vendoredExecutableCount: 1,
      executionCodes: [],
      unstagedExecutionCodes: [],
      requiresExternalProcess: false,
    },
    pythonRuntime: { references: [], remapReferences: [], uiReferences: [], requiresProcess: false },
  },
  "synthetic runtime-boundary analysis",
);

const mixedUserMBoundary = analyzeIniRuntimeBoundaries({
  manifestText: [
    "configs/sim/example/test.ini",
    "configs/sim/example/test.ngc",
    "configs/sim/example/M123",
  ].join("\n"),
  sourceRootRel: "configs/sim/example",
  sourceSearchRootRel: "configs/sim",
  iniFile: "test.ini",
  iniText: [
    "[DISPLAY]",
    "DISPLAY = axis",
    "[RS274NGC]",
    "USER_M_PATH = .",
  ].join("\n"),
  executionTexts: ["M123\nM124\n"],
});
assert.equal(
  mixedUserMBoundary.recommendedBlockedKind,
  "L4-USER-M-PROCESS",
  "mixed vendored and unstaged user-M calls must block",
);
assert.deepEqual(
  mixedUserMBoundary.userMRuntime.executionCodes,
  ["M123", "M124"],
  "mixed user-M execution codes",
);
assert.deepEqual(
  mixedUserMBoundary.userMRuntime.unstagedExecutionCodes,
  ["M124"],
  "only unstaged user-M execution codes require the external process boundary",
);
assert.deepEqual(
  syntheticPlan.files.map((file) => [file.sourceRel, file.wasmPath]).sort(),
  [
    ["configs/sim/example/M123", "/work/sim/example/M123"],
    ["configs/sim/example/linuxcnc.var", "/work/sim/example/linuxcnc.var"],
    ["configs/sim/example/subs/helper.ngc", "/work/sim/example/subs/helper.ngc"],
    ["configs/sim/example/test.ini", "/work/sim/example/test.ini"],
    ["configs/sim/example/test.ngc", "/work/sim/example/test.ngc"],
    ["configs/sim/example/tool.tbl", "/work/sim/example/tool.tbl"],
  ],
  "synthetic sim-config staging wasm paths",
);

const genericPlan = planIniFileContextStaging({
  manifestText: [
    "tests/interp/example/test.ini",
    "tests/interp/example/test.ngc",
    "tests/interp/example/test.tbl",
    "tests/interp/example/startup.var",
    "tests/interp/example/subs/helper.ngc",
    "tests/interp/example/subs/rm145.ngc",
    "tests/interp/example/more-subs/other.ngc",
    "tests/interp/example/M145",
  ].join("\n"),
  sourceRootRel: "tests/interp/example",
  iniFile: "test.ini",
  iniText: [
    "[DISPLAY]",
    "OPEN_FILE = test.ngc",
    "[RS274NGC]",
    "PARAMETER_FILE = startup.var",
    "SUBROUTINE_PATH = subs:more-subs",
    "USER_M_PATH = .",
    "REMAP = M145 modalgroup=10 ngc=rm145",
    "[EMCIO]",
    "TOOL_TABLE = test.tbl",
  ].join("\n"),
  wasmDir: "/work/interp/example",
});
assert.deepEqual(
  genericPlan.files.map((file) => [file.sourceRel, file.wasmPath, file.executable]).sort(),
  [
    ["tests/interp/example/M145", "/work/interp/example/M145", true],
    ["tests/interp/example/more-subs/other.ngc", "/work/interp/example/more-subs/other.ngc", false],
    ["tests/interp/example/startup.var", "/work/interp/example/startup.var", false],
    ["tests/interp/example/subs/helper.ngc", "/work/interp/example/subs/helper.ngc", false],
    ["tests/interp/example/subs/rm145.ngc", "/work/interp/example/subs/rm145.ngc", false],
    ["tests/interp/example/test.ini", "/work/interp/example/test.ini", false],
    ["tests/interp/example/test.ngc", "/work/interp/example/test.ngc", false],
    ["tests/interp/example/test.tbl", "/work/interp/example/test.tbl", false],
  ],
  "generic INI file-context staging plan",
);

function simBoundary(machineRel, iniFile, executionRelFiles = []) {
  return analyzeIniRuntimeBoundaries({
    manifestText,
    sourceRootRel: `configs/sim/${machineRel}`,
    sourceSearchRootRel: "configs/sim",
    iniFile,
    iniText: readSimIni(machineRel, iniFile),
    executionTexts: executionRelFiles.map((relFile) =>
      readFileSync(resolve(vendorRoot, `configs/sim/${machineRel}/${relFile}`), "utf8"),
    ),
  });
}

const dbDemoBoundary = simBoundary("axis/db_demo", "db_nonran.ini");
assert.deepEqual(
  dbDemoBoundary.dependencies,
  ["python_runtime", "tool_database_process"],
  "db_demo tool database boundary dependencies",
);
assert.equal(dbDemoBoundary.recommendedBlockedKind, "L4-TOOL-DB", "db_demo blocked kind");
assert.equal(dbDemoBoundary.toolDatabaseProgram, "./db_nonran.py", "db_demo DB_PROGRAM");
assert.deepEqual(
  dbDemoBoundary.pythonRuntime.uiReferences,
  ["./db_nonran.py"],
  "db_demo Python dependency is the DB program process",
);
assert.deepEqual(
  dbDemoBoundary.pythonRuntime.remapReferences,
  [],
  "db_demo does not declare Python remap runtime",
);

const millturnBoundary = simBoundary("axis/vismach/millturn", "millturn.ini", [
  "example.ngc",
  "remap_subs/428remap.ngc",
  "remap_subs/429remap.ngc",
]);
assert.equal(
  millturnBoundary.recommendedBlockedKind,
  "L4-USER-M-PROCESS",
  "millturn blocked kind",
);
assert.deepEqual(
  millturnBoundary.dependencies,
  ["external_user_m_process", "hal_process", "halui_mdi_process", "ui_process"],
  "millturn full-process boundary dependencies",
);
assert.equal(
  millturnBoundary.userMRuntime.vendoredExecutableCount,
  0,
  "millturn external user-M files are not staged",
);
assert.deepEqual(
  millturnBoundary.userMRuntime.executionCodes,
  ["M128", "M129"],
  "millturn execution chain calls external user-M processes",
);
assert.deepEqual(
  millturnBoundary.userMRuntime.unstagedExecutionCodes,
  ["M128", "M129"],
  "millturn execution chain calls unstaged user-M processes",
);

const pumaBoundary = simBoundary("axis/vismach/puma", "puma_cube.ini");
assert.equal(pumaBoundary.recommendedBlockedKind, "-", "puma_cube representative is not blocked");
assert.equal(pumaBoundary.userMRuntime.vendoredExecutableCount, 0, "puma_cube has no vendored user-M processes");
assert.equal(pumaBoundary.halRuntime.requiresProcess, true, "puma_cube declares HAL process files");
assert.equal(pumaBoundary.haluiRuntime.requiresProcess, true, "puma_cube declares HALUI MDI commands");

const melfaBoundary = simBoundary("axis/vismach/melfa-sim", "melfa.ini");
assert.equal(melfaBoundary.recommendedBlockedKind, "-", "melfa representative is not blocked");
assert.deepEqual(
  melfaBoundary.dependencies,
  ["hal_process", "halui_mdi_process", "ui_process"],
  "melfa declares HAL/UI/HALUI process dependencies",
);
assert.equal(melfaBoundary.pythonRuntime.requiresProcess, false, "melfa representative has no Python runtime");

const woodpeckerBoundary = simBoundary("woodpecker", "woodpecker.ini");
assert.equal(woodpeckerBoundary.recommendedBlockedKind, "-", "woodpecker representative is not blocked");
assert.deepEqual(
  woodpeckerBoundary.dependencies,
  ["hal_process", "ui_process"],
  "woodpecker declares HAL/UI process dependencies",
);
assert.equal(woodpeckerBoundary.haluiRuntime.requiresProcess, false, "woodpecker representative has no HALUI MDI");

const gladevcpBoundary = simBoundary("axis/gladevcp", "gladevcp_panel.ini");
assert.equal(gladevcpBoundary.recommendedBlockedKind, "-", "gladevcp probe representative is not blocked");
assert.ok(
  gladevcpBoundary.dependencies.includes("ui_process"),
  "gladevcp declares UI process dependency",
);
assert.equal(gladevcpBoundary.pythonRuntime.requiresProcess, true, "gladevcp declares Python UI handler");
assert.deepEqual(
  gladevcpBoundary.pythonRuntime.uiReferences,
  ["-u hitcounter.py manual-example.ui"],
  "gladevcp Python dependency is a UI handler",
);
assert.deepEqual(
  gladevcpBoundary.pythonRuntime.remapReferences,
  [],
  "gladevcp representative must not be treated as Python remap runtime",
);

function simMachine(machineRel, iniFile, programFile = null) {
  const iniText = readFileSync(
    resolve(vendorRoot, `configs/sim/${machineRel}/${iniFile}`),
    "utf8",
  );
  const plan = planSimConfigStaging({
    manifestText,
    machineRel,
    iniFile,
    iniText,
    programFile,
    wasmDir: `/work/sim/${machineRel}`,
  });
  return {
    ...plan,
    files: plan.files.map((file) => ({
      path: file.path,
      text: readFileSync(resolve(vendorRoot, file.sourceRel), "utf8"),
      executable:
        file.executable || (statSync(resolve(vendorRoot, file.sourceRel)).mode & 0o111) !== 0,
    })),
  };
}

function wrapSimProgram(machine, wrappedName) {
  const sourceFile = machine.files.find((file) => file.path === machine.programPath);
  assert.ok(sourceFile, `${machine.programPath}: wrapped source file`);
  return {
    ...machine,
    programPath: `${machine.wasmDir}/${wrappedName}`,
    files: [
      ...machine.files,
      {
        path: `${machine.wasmDir}/${wrappedName}`,
        text: `${sourceFile.text}\nM2\n`,
        executable: false,
      },
    ],
  };
}

const foam = simMachine("axis/foam", "axis_foam.ini", "foam.ngc");
verifyExpectedOutput(
  "sim_axis_foam_uv_wasm",
  interp.runSimConfigProgram({
    files: foam.files,
    programPath: foam.programPath,
    iniPath: foam.iniPath,
  }),
  [
    "file_open=0",
    "file_saw_error=0",
    "canon_event=PROGRAM_END",
    "absent=Bad character 'u' used",
    "absent=Bad character 'v' used",
  ].join("\n"),
);

const bridgeMill = simMachine(
  "axis/vismach/5axis/bridgemill",
  "5axis.ini",
  "5axisgui.ngc",
);
verifyExpectedOutput(
  "sim_axis_bridgemill_w_wasm",
  interp.runSimConfigProgram({
    files: bridgeMill.files,
    programPath: bridgeMill.programPath,
    iniPath: bridgeMill.iniPath,
    executionMode: "fiveAxisRemap",
  }),
  [
    "fiveaxis_ini_open=1",
    "fiveaxis_tool_table_load=0",
    "fiveaxis_parse_remap_1=0",
    "fiveaxis_parse_remap_2=0",
    "fiveaxis_parse_remap_3=0",
    "fiveaxis_parsed_remap_count=3",
    "fiveaxis_remaps_ready=1",
    "fiveaxis_file_open=0",
    "fiveaxis_file_read_count=2197",
    "fiveaxis_file_execute_count=2197",
    "fiveaxis_file_finish_count=137",
    "fiveaxis_file_reached_exit=1",
    "fiveaxis_hal_switchkins: rc=0 found=1 value=0",
    "fiveaxis_linuxcnc_remap_file_execute=1",
    "absent=Bad character 'w' used",
  ].join("\n"),
);

const geometry = simMachine("axis/geometry", "xyzc.ini", "xyzc.ngc");
verifyExpectedOutput(
  "sim_axis_geometry_xyzc_user_m110_wasm",
  interp.runSimConfigProgram({
    files: geometry.files,
    programPath: geometry.programPath,
    iniPath: geometry.iniPath,
  }),
  [
    "file_open=0",
    "file_saw_error=0",
    "canon_event=USER_M_COMMAND code=M110",
    "canon_event=PROGRAM_END",
    "absent=Unknown m code used: M110",
    "absent=Bad character 'c' used",
  ].join("\n"),
);

const gladevcpProbe = simMachine("axis/gladevcp", "gladevcp_panel.ini", "probe.ngc");
verifyExpectedOutput(
  "sim_axis_gladevcp_probe_wasm",
  interp.runSimConfigProgram({
    files: gladevcpProbe.files,
    programPath: gladevcpProbe.programPath,
    iniPath: gladevcpProbe.iniPath,
  }),
  [
    "file_open=0",
    "file_saw_error=0",
    "canon_event=PROGRAM_END",
    "absent=file_error_text=",
  ].join("\n"),
);

const externalOffsetsMacro = wrapSimProgram(
  simMachine("axis/external_offsets", "dynamic_offsets.ini", "circles.ngc"),
  "__wrapped_circles.ngc",
);
verifyExpectedOutput(
  "sim_axis_external_offsets_circles_macro_wasm",
  interp.runSimConfigProgram({
    files: externalOffsetsMacro.files,
    programPath: externalOffsetsMacro.programPath,
    iniPath: externalOffsetsMacro.iniPath,
  }),
  [
    "file_open=0",
    "file_execute_45=1",
    "statement_uri=M2",
    "file_read_count=45",
    "file_execute_count=45",
    "file_saw_error=0",
    "canon_event=COMMENT: info: Multiple circles at radius R, use 0 for current val",
    "canon_event=PROGRAM_END",
    "post_execute.origin_index=1",
    "post_execute.mist=0",
    "post_execute.flood=0",
    "absent=file_error_text=",
  ].join("\n"),
);

for (const externalOffsetCase of [
  {
    name: "dyn_demo",
    program: "dyn_demo.ngc",
    ini: "dynamic_offsets.ini",
    readCount: 833,
    firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=21 x=0 y=0 z=0",
    firstFeed: "canon_event=STRAIGHT_FEED line=26 x=9 y=0 z=1.5",
  },
  {
    name: "eoffsets",
    program: "eoffsets.ngc",
    ini: "eoffsets.ini",
    readCount: 35,
    firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=24 x=0 y=0 z=0",
    firstFeed: "canon_event=STRAIGHT_FEED line=27 x=2.25 y=2.64 z=0",
  },
  {
    name: "jwp_z",
    program: "jwp_z.ngc",
    ini: "jwp_z.ini",
    readCount: 35,
    firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=17 x=0 y=0 z=0.5",
    firstFeed: "canon_event=STRAIGHT_FEED line=24 x=2.5 y=3.3 z=0",
  },
  {
    name: "opa_demo",
    program: "opa_demo.ngc",
    ini: "opa.ini",
    readCount: 58,
    firstTraverse: "canon_event=STRAIGHT_TRAVERSE line=13 x=1 y=0 z=0",
    firstFeed: "canon_event=STRAIGHT_FEED line=35 x=1 y=0 z=0",
  },
]) {
  const externalOffsets = simMachine(
    "axis/external_offsets",
    externalOffsetCase.ini,
    externalOffsetCase.program,
  );
  verifyExpectedOutput(
    `sim_axis_external_offsets_${externalOffsetCase.name}_user_m111_wasm`,
    interp.runSimConfigProgram({
      files: externalOffsets.files,
      programPath: externalOffsets.programPath,
      iniPath: externalOffsets.iniPath,
    }),
    [
      "file_open=0",
      `file_read_count=${externalOffsetCase.readCount}`,
      `file_execute_count=${externalOffsetCase.readCount}`,
      "file_saw_error=0",
      "canon_event=USER_M_COMMAND code=M111",
      externalOffsetCase.firstTraverse,
      externalOffsetCase.firstFeed,
      "canon_event=PROGRAM_END",
      "absent=Unknown m code used: M111",
      "absent=Bad character",
      "absent=Cannot use axis values",
    ].join("\n"),
  );
}

const melfaSim = simMachine("axis/vismach/melfa-sim", "melfa.ini", "example.ngc");
verifyExpectedOutput(
  "sim_axis_vismach_melfa_wasm",
  interp.runSimConfigProgram({
    files: melfaSim.files,
    programPath: melfaSim.programPath,
    iniPath: melfaSim.iniPath,
    executionMode: "fiveAxisRemap",
  }),
  [
    "fiveaxis_ini_open=1",
    "fiveaxis_tool_table_load=0",
    "fiveaxis_file_open=0",
    "fiveaxis_file_reached_exit=1",
    "fiveaxis_linuxcnc_remap_file_execute=1",
  ].join("\n"),
);

const pumaCube = simMachine("axis/vismach/puma", "puma_cube.ini", "puma_cube.ngc");
verifyExpectedOutput(
  "sim_axis_vismach_puma_cube_wasm",
  interp.runSimConfigProgram({
    files: pumaCube.files,
    programPath: pumaCube.programPath,
    iniPath: pumaCube.iniPath,
  }),
  [
    "file_open=0",
    "file_saw_error=0",
    "canon_event=PROGRAM_END",
    "absent=file_error_text=",
  ].join("\n"),
);

const woodpeckerOnAbort = simMachine("woodpecker", "woodpecker.ini", "on_abort.ngc");
verifyExpectedOutput(
  "sim_woodpecker_on_abort_wasm",
  interp.runSimConfigProgram({
    files: woodpeckerOnAbort.files,
    programPath: woodpeckerOnAbort.programPath,
    iniPath: woodpeckerOnAbort.iniPath,
  }),
  [
    "file_open=0",
    "run_step phase=execute step=17 rc=1 line=17",
    "statement_uri=m2",
    "file_read_count=17",
    "file_execute_count=17",
    "file_saw_error=0",
    "canon_event=ON_RESET",
    "canon_event=PROGRAM_END",
    "post_execute.origin_index=1",
    "post_execute.mist=0",
    "post_execute.flood=0",
    "absent=file_error_text=",
  ].join("\n"),
);

console.log("sim_configs_wasm_node_smoke=ok");
