import assert from "node:assert/strict";

import {
  getOpfsRoot,
  loadTextFile,
  saveTextFile,
} from "../../../runtime/opfs/file-service.js";
import {
  defaultMachinePaths,
  gcodeProgramPath,
  machineIniPath,
  normalizeOpfsPath,
  parameterFilePath,
  previewCachePath,
  sessionSnapshotPath,
  toolTablePath,
} from "../../../runtime/opfs/path-model.js";
import {
  createMachineSessionSnapshotPayload,
  createSessionSnapshot,
  loadMachineSessionSnapshot,
  loadSessionSnapshot,
  saveMachineSessionSnapshot,
  saveSessionSnapshot,
  validateSessionSnapshot,
} from "../../../runtime/opfs/snapshot-store.js";
import {
  gcodeFilenameFromProgramPath,
  loadGcodeProgram,
  loadMachineTextFiles,
  machineFilePaths,
  saveGcodeProgram,
  saveMachineTextFiles,
} from "../../../runtime/opfs/machine-file-store.js";
import {
  restoreMachineParametersFromOpfs,
  saveMachineParametersToOpfs,
} from "../../../runtime/opfs/linuxcnc-parameter-bridge.js";
import {
  loadMachineToolTableFromOpfs,
  saveMachineToolTableToOpfs,
} from "../../../runtime/opfs/linuxcnc-tool-table-bridge.js";
import {
  loadMachineSessionFromOpfs,
} from "../../../runtime/opfs/linuxcnc-machine-session-bridge.js";
import {
  readMachineSessionReadiness,
} from "../../../runtime/opfs/machine-session-readiness.js";

class MockFileHandle {
  constructor(name) {
    this.name = name;
    this.textValue = "";
  }

  async createWritable() {
    return {
      write: async (text) => {
        this.textValue = String(text);
      },
      close: async () => {},
    };
  }

  async getFile() {
    return {
      text: async () => this.textValue,
    };
  }
}

class MockDirectoryHandle {
  constructor(name = "") {
    this.name = name;
    this.dirs = new Map();
    this.files = new Map();
  }

  async getDirectoryHandle(name, options = {}) {
    if (!this.dirs.has(name)) {
      if (!options.create) {
        throw new Error(`missing directory: ${name}`);
      }
      this.dirs.set(name, new MockDirectoryHandle(name));
    }
    return this.dirs.get(name);
  }

  async getFileHandle(name, options = {}) {
    if (!this.files.has(name)) {
      if (!options.create) {
        throw new Error(`missing file: ${name}`);
      }
      this.files.set(name, new MockFileHandle(name));
    }
    return this.files.get(name);
  }
}

const root = new MockDirectoryHandle();
const storage = {
  getDirectory: async () => root,
};

function assertGcodeProgramStoragePath(filename, path) {
  assert.equal(
    path,
    `linuxcnc/gcode/${filename}`,
    `${filename} G-code OPFS path drift`,
  );
}

function assertSessionSnapshotStoragePath(sessionId, path, filename = "snapshot.json") {
  assert.equal(
    path,
    `linuxcnc/sessions/${sessionId}/${filename}`,
    `${sessionId}/${filename} session snapshot OPFS path drift`,
  );
}

function assertPreviewCacheStoragePath(cacheKey, path, filename = "preview.json") {
  assert.equal(
    path,
    `linuxcnc/preview-cache/${cacheKey}/${filename}`,
    `${cacheKey}/${filename} preview cache OPFS path drift`,
  );
}

function assertMachineFileStoragePaths(machineId, paths) {
  assert.deepEqual(
    paths,
    {
      ini: `linuxcnc/machines/${machineId}/machine.ini`,
      toolTable: `linuxcnc/machines/${machineId}/tool.tbl`,
      parameters: `linuxcnc/machines/${machineId}/linuxcnc.var`,
    },
    `${machineId} machine file OPFS path drift`,
  );
}

function assertParameterBridgePaths(
  machineId,
  result,
  wasmPath,
  filename = "linuxcnc.var",
) {
  assert.equal(
    result.opfsPath,
    `linuxcnc/machines/${machineId}/${filename}`,
    `${machineId}/${filename} parameter bridge OPFS path drift`,
  );
  assert.equal(
    result.wasmPath,
    wasmPath,
    `${machineId}/${filename} parameter bridge WASM path drift`,
  );
}

function assertToolTableBridgePaths(
  machineId,
  result,
  wasmPath,
  filename = "tool.tbl",
) {
  assert.equal(
    result.opfsPath,
    `linuxcnc/machines/${machineId}/${filename}`,
    `${machineId}/${filename} tool table bridge OPFS path drift`,
  );
  assert.equal(
    result.wasmPath,
    wasmPath,
    `${machineId}/${filename} tool table bridge WASM path drift`,
  );
}

function assertSessionBridgePaths(
  machineId,
  result,
  {
    iniWasmPath = "/work/machine.ini",
    parameterWasmPath = "/work/linuxcnc.var",
    toolTableWasmPath = "/work/tool.tbl",
  } = {},
) {
  assert.equal(result.machineId, machineId, `${machineId} session machine id drift`);
  assert.equal(
    result.ini.opfsPath,
    `linuxcnc/machines/${machineId}/machine.ini`,
    `${machineId} session INI OPFS path drift`,
  );
  assert.equal(result.ini.wasmPath, iniWasmPath, `${machineId} session INI WASM path drift`);
  assertParameterBridgePaths(machineId, result.parameters, parameterWasmPath);
  assertToolTableBridgePaths(machineId, result.toolTable, toolTableWasmPath);
}

assert.equal(await getOpfsRoot(storage), root);

assert.equal(normalizeOpfsPath("linuxcnc/machines/xyzab.ini"), "linuxcnc/machines/xyzab.ini");
assert.equal(machineIniPath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/machine.ini");
assert.equal(toolTablePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/tool.tbl");
assert.equal(parameterFilePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/linuxcnc.var");
assertGcodeProgramStoragePath("fixture.ngc", gcodeProgramPath("fixture.ngc"));
assertGcodeProgramStoragePath(
  "custom-fixture.ngc",
  gcodeProgramPath("custom-fixture.ngc"),
);
assert.equal(
  gcodeFilenameFromProgramPath("linuxcnc/gcode/fixture.ngc"),
  "fixture.ngc",
);
assert.throws(
  () => gcodeFilenameFromProgramPath("linuxcnc/machines/xyzab-tdr/fixture.ngc"),
  /Invalid G-code program OPFS path/,
);
assert.throws(
  () => gcodeFilenameFromProgramPath("linuxcnc/gcode/nested/fixture.ngc"),
  /Invalid G-code program OPFS path/,
);
assertPreviewCacheStoragePath("fixture", previewCachePath("fixture"));
assertPreviewCacheStoragePath(
  "fixture",
  previewCachePath("fixture", "custom-preview.json"),
  "custom-preview.json",
);
assertSessionSnapshotStoragePath(
  "session-1",
  sessionSnapshotPath("session-1"),
);
assertSessionSnapshotStoragePath(
  "session-1",
  sessionSnapshotPath("session-1", "custom-snapshot.json"),
  "custom-snapshot.json",
);
assertMachineFileStoragePaths("xyzab-tdr", defaultMachinePaths("xyzab-tdr"));
assertMachineFileStoragePaths("xyzab-tdr", machineFilePaths("xyzab-tdr"));

const snapshotPayload = createMachineSessionSnapshotPayload("xyzab-tdr", {
  gcodeFilename: "fixture.ngc",
});
assert.deepEqual(snapshotPayload, {
  machineId: "xyzab-tdr",
  files: {
    ini: machineIniPath("xyzab-tdr"),
    parameters: parameterFilePath("xyzab-tdr"),
    toolTable: toolTablePath("xyzab-tdr"),
    gcode: gcodeProgramPath("fixture.ngc"),
  },
});
assert.deepEqual(
  createMachineSessionSnapshotPayload("custom-machine", {
    parameterFilename: "custom.var",
    toolTableFilename: "custom-tool.tbl",
    gcodeOpfsPath: "linuxcnc/gcode/custom-fixture.ngc",
  }),
  {
    machineId: "custom-machine",
    files: {
      ini: "linuxcnc/machines/custom-machine/machine.ini",
      parameters: "linuxcnc/machines/custom-machine/custom.var",
      toolTable: "linuxcnc/machines/custom-machine/custom-tool.tbl",
      gcode: "linuxcnc/gcode/custom-fixture.ngc",
    },
  },
);
assert.throws(
  () => createMachineSessionSnapshotPayload("bad-machine", { gcodeFilename: "../escape.ngc" }),
  /Invalid G-code filename/,
);
const snapshot = createSessionSnapshot("session-1", snapshotPayload, {
  createdAt: "2026-06-08T00:00:00.000Z",
  metadata: { source: "node-smoke" },
});
assert.deepEqual(snapshot, {
  format: "linuxcnc-wasm-session-snapshot",
  version: 1,
  sessionId: "session-1",
  createdAt: "2026-06-08T00:00:00.000Z",
  metadata: { source: "node-smoke" },
  payload: snapshotPayload,
});
assert.equal(validateSessionSnapshot(snapshot, "session-1"), snapshot);
assert.throws(
  () => validateSessionSnapshot({ ...snapshot, format: "other-format" }, "session-1"),
  /Unsupported session snapshot format/,
);
assert.throws(
  () => validateSessionSnapshot({ ...snapshot, version: 99 }, "session-1"),
  /Unsupported session snapshot version/,
);
assert.throws(
  () => validateSessionSnapshot({ ...snapshot, sessionId: "other-session" }, "session-1"),
  /Session snapshot id mismatch/,
);
assert.throws(
  () => validateSessionSnapshot({ ...snapshot, metadata: null }, "session-1"),
  /snapshot metadata must be a plain object/,
);
assert.throws(
  () => validateSessionSnapshot({ ...snapshot, payload: [] }, "session-1"),
  /snapshot payload must be a plain object/,
);

await saveTextFile(
  "linuxcnc/machines/xyzab.ini",
  "[EMC]\nMACHINE = opfs-smoke\n",
  storage,
);
assert.equal(
  await loadTextFile("linuxcnc/machines/xyzab.ini", storage),
  "[EMC]\nMACHINE = opfs-smoke\n",
);

await saveSessionSnapshot("session-1", snapshotPayload, {
  storage,
  createdAt: "2026-06-08T00:00:00.000Z",
  metadata: { source: "node-smoke" },
});
assertSessionSnapshotStoragePath(
  "session-1",
  sessionSnapshotPath("session-1"),
);
assert.deepEqual(await loadSessionSnapshot("session-1", { storage }), snapshot);
const customSnapshot = await saveSessionSnapshot("session-1", snapshotPayload, {
  storage,
  filename: "custom-snapshot.json",
  createdAt: "2026-06-08T00:00:00.000Z",
  metadata: { source: "node-smoke-custom" },
});
assertSessionSnapshotStoragePath(
  "session-1",
  sessionSnapshotPath("session-1", "custom-snapshot.json"),
  "custom-snapshot.json",
);
assert.equal(
  customSnapshot.metadata.source,
  "node-smoke-custom",
);
assert.deepEqual(
  await loadSessionSnapshot("session-1", {
    storage,
    filename: "custom-snapshot.json",
  }),
  customSnapshot,
);
const machineSessionSnapshot = await saveMachineSessionSnapshot(
  "machine-session-1",
  "xyzab-tdr",
  {
    storage,
    filename: "machine-session-snapshot.json",
    gcodeFilename: "fixture.ngc",
    createdAt: "2026-06-08T00:00:00.000Z",
    metadata: { source: "node-machine-session" },
  },
);
assertSessionSnapshotStoragePath(
  "machine-session-1",
  sessionSnapshotPath("machine-session-1", "machine-session-snapshot.json"),
  "machine-session-snapshot.json",
);
assert.deepEqual(machineSessionSnapshot.payload, snapshotPayload);
assert.deepEqual(
  await loadSessionSnapshot("machine-session-1", {
    storage,
    filename: "machine-session-snapshot.json",
  }),
  machineSessionSnapshot,
);
assert.deepEqual(
  await loadMachineSessionSnapshot("machine-session-1", "xyzab-tdr", {
    storage,
    filename: "machine-session-snapshot.json",
  }),
  machineSessionSnapshot,
);
await assert.rejects(
  () => loadMachineSessionSnapshot("machine-session-1", "other-machine", {
    storage,
    filename: "machine-session-snapshot.json",
  }),
  /Machine session snapshot id mismatch/,
);
await saveTextFile(
  sessionSnapshotPath("machine-session-bad-files"),
  JSON.stringify({
    ...machineSessionSnapshot,
    sessionId: "machine-session-bad-files",
    payload: {
      machineId: "xyzab-tdr",
      files: {
        ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
        parameters: "../linuxcnc.var",
        toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
      },
    },
  }),
  storage,
);
await assert.rejects(
  () => loadMachineSessionSnapshot("machine-session-bad-files", "xyzab-tdr", { storage }),
  /Invalid OPFS path/,
);

await saveMachineTextFiles("xyzab-tdr", {
  ini: "[EMC]\nMACHINE = xyzab-tdr\n",
  toolTable: "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n",
  parameters: "5161 0.0\n5162 0.0\n",
}, { storage });
assert.deepEqual(await loadMachineTextFiles("xyzab-tdr", { storage }), {
  ini: "[EMC]\nMACHINE = xyzab-tdr\n",
  toolTable: "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n",
  parameters: "5161 0.0\n5162 0.0\n",
});
assertGcodeProgramStoragePath(
  "fixture.ngc",
  await saveGcodeProgram("fixture.ngc", "G0 X0 Y0\nM2\n", { storage }),
);
assert.equal(await loadGcodeProgram("fixture.ngc", { storage }), "G0 X0 Y0\nM2\n");
assertGcodeProgramStoragePath(
  "custom-fixture.ngc",
  await saveGcodeProgram("custom-fixture.ngc", "G1 X1 F10\nM2\n", { storage }),
);
assert.equal(await loadGcodeProgram("custom-fixture.ngc", { storage }), "G1 X1 F10\nM2\n");

const readyMachineSession = await readMachineSessionReadiness("xyzab-tdr", {
  storage,
  sessionId: "machine-session-1",
  snapshotFilename: "machine-session-snapshot.json",
  gcodeFilename: "fixture.ngc",
});
assert.equal(readyMachineSession.ready, true);
assert.equal(readyMachineSession.phase, "ready");
assert.deepEqual(readyMachineSession.missing, []);
assert.deepEqual(readyMachineSession.errors, []);
assert.deepEqual(
  readyMachineSession.checks.map((check) => [check.name, check.ok, check.path]),
  [
    ["ini", true, "linuxcnc/machines/xyzab-tdr/machine.ini"],
    ["parameters", true, "linuxcnc/machines/xyzab-tdr/linuxcnc.var"],
    ["toolTable", true, "linuxcnc/machines/xyzab-tdr/tool.tbl"],
    ["gcode", true, "linuxcnc/gcode/fixture.ngc"],
  ],
);
assert.deepEqual(readyMachineSession.snapshotCheck, {
  name: "snapshot",
  path: "linuxcnc/sessions/machine-session-1/machine-session-snapshot.json",
  ok: true,
  error: null,
});
assert.equal(readyMachineSession.snapshot.sessionId, "machine-session-1");

const missingMachineSession = await readMachineSessionReadiness("missing-machine", {
  storage,
  sessionId: "missing-session",
  gcodeRequired: true,
  gcodeFilename: "missing.ngc",
});
assert.equal(missingMachineSession.ready, false);
assert.equal(missingMachineSession.phase, "blocked");
assert.deepEqual(missingMachineSession.missing, [
  "ini",
  "parameters",
  "toolTable",
  "gcode",
  "snapshot",
]);
assert.deepEqual(
  missingMachineSession.errors.map((error) => error.name),
  ["ini", "parameters", "toolTable", "gcode", "snapshot"],
);
assert.match(missingMachineSession.errors[0].error, /missing directory|missing file/);

const bridgeFiles = new Map();
const bridgeInterp = {
  writeTextFile(path, text) {
    bridgeFiles.set(path, text);
  },
  readTextFile(path) {
    if (!bridgeFiles.has(path)) {
      throw new Error(`missing wasm file: ${path}`);
    }
    return bridgeFiles.get(path);
  },
  restoreParameters(path) {
    return `restore_parameters=0\nrestore_path=${path}\n`;
  },
  saveParameters(path, values) {
    bridgeFiles.set(path, "5161\t12.340000\n5162\t56.780000\n");
    bridgeFiles.set(`${path}.bak`, "5161 0.0\n5162 0.0\n");
    return `save_parameters=0\nsave_path=${path}\nvalues=${Object.keys(values).join(",")}\n`;
  },
  loadToolTable(path, options = {}) {
    return [
      "tooldata_load=0",
      `load_tool_path=${path}`,
      `random_toolchanger=${options.randomToolChanger === true ? 1 : 0}`,
      "",
    ].join("\n");
  },
  saveToolTable(path) {
    bridgeFiles.set(path, "T2   P2   Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n");
    return `tooldata_save=0\nsave_tool_path=${path}\n`;
  },
};

const bridgeIniSdk = {
  writeTextFile(path, text) {
    bridgeFiles.set(`ini:${path}`, text);
  },
  getString(path, section, tag) {
    if (
      path === "/work/ini-file-session.ini" &&
      section === "RS274NGC" &&
      tag === "PARAMETER_FILE"
    ) {
      return "custom.var";
    }
    if (
      path === "/work/ini-override-session.ini" &&
      section === "RS274NGC" &&
      tag === "PARAMETER_FILE"
    ) {
      return "ignored.var";
    }
    if (
      path === "/work/invalid-ini-file-session.ini" &&
      section === "RS274NGC" &&
      tag === "PARAMETER_FILE"
    ) {
      return "../escape.var";
    }
    if (
      path === "/work/ini-file-session.ini" &&
      section === "EMCIO" &&
      tag === "TOOL_TABLE"
    ) {
      return "custom-tool.tbl";
    }
    if (
      path === "/work/ini-override-session.ini" &&
      section === "EMCIO" &&
      tag === "TOOL_TABLE"
    ) {
      return "ignored-tool.tbl";
    }
    if (
      path === "/work/invalid-tool-file-session.ini" &&
      section === "EMCIO" &&
      tag === "TOOL_TABLE"
    ) {
      return "nested/tool.tbl";
    }
    return null;
  },
  getBool(path, section, tag) {
    if (
      path === "/work/ini-derived-session.ini" &&
      section === "EMCIO" &&
      tag === "RANDOM_TOOLCHANGER"
    ) {
      return true;
    }
    return null;
  },
};

const restoredParameters = await restoreMachineParametersFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { storage, wasmPath: "/work/bridge.var" },
);
assert.deepEqual(restoredParameters, {
  opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
  wasmPath: "/work/bridge.var",
  result: "restore_parameters=0\nrestore_path=/work/bridge.var\n",
});
assert.equal(bridgeFiles.get("/work/bridge.var"), "5161 0.0\n5162 0.0\n");
assertParameterBridgePaths("xyzab-tdr", restoredParameters, "/work/bridge.var");

const restoredDefaultParameters = await restoreMachineParametersFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { storage },
);
assertParameterBridgePaths("xyzab-tdr", restoredDefaultParameters, "/work/linuxcnc.var");
assert.equal(
  restoredDefaultParameters.result,
  "restore_parameters=0\nrestore_path=/work/linuxcnc.var\n",
);
assert.equal(bridgeFiles.get("/work/linuxcnc.var"), "5161 0.0\n5162 0.0\n");

const savedParameters = await saveMachineParametersToOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { 5161: 12.34, 5162: 56.78 },
  { storage, wasmPath: "/work/bridge.var" },
);
assertParameterBridgePaths("xyzab-tdr", savedParameters, "/work/bridge.var");
assert.equal(savedParameters.backupOpfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak");
assert.equal(savedParameters.result.includes("save_parameters=0"), true);
assert.equal(
  await loadTextFile("linuxcnc/machines/xyzab-tdr/linuxcnc.var", storage),
  "5161\t12.340000\n5162\t56.780000\n",
);
assert.equal(
  await loadTextFile("linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak", storage),
  "5161 0.0\n5162 0.0\n",
);
const savedDefaultParameters = await saveMachineParametersToOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { 5161: 12.34 },
  { storage },
);
assertParameterBridgePaths("xyzab-tdr", savedDefaultParameters, "/work/linuxcnc.var");
assert.equal(
  savedDefaultParameters.backupOpfsPath,
  "linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak",
);
assert.equal(savedDefaultParameters.result.includes("save_path=/work/linuxcnc.var"), true);

const loadedToolTable = await loadMachineToolTableFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { storage, wasmPath: "/work/bridge-tool.tbl" },
);
assert.deepEqual(loadedToolTable, {
  opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
  wasmPath: "/work/bridge-tool.tbl",
  result: "tooldata_load=0\nload_tool_path=/work/bridge-tool.tbl\nrandom_toolchanger=0\n",
});
assert.equal(bridgeFiles.get("/work/bridge-tool.tbl"), "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n");
assertToolTableBridgePaths("xyzab-tdr", loadedToolTable, "/work/bridge-tool.tbl");

const loadedDefaultToolTable = await loadMachineToolTableFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { storage },
);
assertToolTableBridgePaths("xyzab-tdr", loadedDefaultToolTable, "/work/tool.tbl");
assert.equal(
  loadedDefaultToolTable.result,
  "tooldata_load=0\nload_tool_path=/work/tool.tbl\nrandom_toolchanger=0\n",
);
assert.equal(bridgeFiles.get("/work/tool.tbl"), "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n");

const loadedRandomToolTable = await loadMachineToolTableFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { storage, wasmPath: "/work/random-bridge-tool.tbl", randomToolChanger: true },
);
assert.deepEqual(loadedRandomToolTable, {
  opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
  wasmPath: "/work/random-bridge-tool.tbl",
  result:
    "tooldata_load=0\nload_tool_path=/work/random-bridge-tool.tbl\nrandom_toolchanger=1\n",
});

const savedToolTable = await saveMachineToolTableToOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { storage, wasmPath: "/work/bridge-tool.tbl" },
);
assert.equal(savedToolTable.opfsPath, "linuxcnc/machines/xyzab-tdr/tool.tbl");
assert.equal(savedToolTable.wasmPath, "/work/bridge-tool.tbl");
assertToolTableBridgePaths("xyzab-tdr", savedToolTable, "/work/bridge-tool.tbl");
assert.equal(savedToolTable.result.includes("tooldata_save=0"), true);
assert.equal(
  await loadTextFile("linuxcnc/machines/xyzab-tdr/tool.tbl", storage),
  "T2   P2   Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n",
);
const savedDefaultToolTable = await saveMachineToolTableToOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { storage },
);
assertToolTableBridgePaths("xyzab-tdr", savedDefaultToolTable, "/work/tool.tbl");
assert.equal(savedDefaultToolTable.result.includes("save_tool_path=/work/tool.tbl"), true);

const loadedSession = await loadMachineSessionFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  {
    storage,
    iniWasmPath: "/work/session.ini",
    parameterWasmPath: "/work/session.var",
    toolTableWasmPath: "/work/session-tool.tbl",
  },
);
assert.deepEqual(loadedSession, {
  machineId: "xyzab-tdr",
  ini: {
    opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
    wasmPath: "/work/session.ini",
  },
  parameters: {
    opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
    wasmPath: "/work/session.var",
    result: "restore_parameters=0\nrestore_path=/work/session.var\n",
  },
  toolTable: {
    opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
    wasmPath: "/work/session-tool.tbl",
    result: "tooldata_load=0\nload_tool_path=/work/session-tool.tbl\nrandom_toolchanger=0\n",
  },
});
assertSessionBridgePaths("xyzab-tdr", loadedSession, {
  iniWasmPath: "/work/session.ini",
  parameterWasmPath: "/work/session.var",
  toolTableWasmPath: "/work/session-tool.tbl",
});
assert.equal(bridgeFiles.get("/work/session.ini"), "[EMC]\nMACHINE = xyzab-tdr\n");
assert.equal(bridgeFiles.get("/work/session.var"), "5161\t12.340000\n5162\t56.780000\n");
assert.equal(
  bridgeFiles.get("/work/session-tool.tbl"),
  "T2   P2   Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n",
);

const loadedDefaultSession = await loadMachineSessionFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  { storage },
);
assertSessionBridgePaths("xyzab-tdr", loadedDefaultSession);
assert.equal(bridgeFiles.get("/work/machine.ini"), "[EMC]\nMACHINE = xyzab-tdr\n");
assert.equal(bridgeFiles.get("/work/linuxcnc.var"), "5161\t12.340000\n5162\t56.780000\n");
assert.equal(
  bridgeFiles.get("/work/tool.tbl"),
  "T2   P2   Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n",
);

const loadedRandomSession = await loadMachineSessionFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  {
    storage,
    iniWasmPath: "/work/random-session.ini",
    parameterWasmPath: "/work/random-session.var",
    toolTableWasmPath: "/work/random-session-tool.tbl",
    randomToolChanger: true,
  },
);
assert.equal(
  loadedRandomSession.toolTable.result,
  "tooldata_load=0\nload_tool_path=/work/random-session-tool.tbl\nrandom_toolchanger=1\n",
);

const loadedIniDerivedRandomSession = await loadMachineSessionFromOpfs(
  bridgeInterp,
  "xyzab-tdr",
  {
    storage,
    iniSdk: bridgeIniSdk,
    iniWasmPath: "/work/ini-derived-session.ini",
    parameterWasmPath: "/work/ini-derived-session.var",
    toolTableWasmPath: "/work/ini-derived-session-tool.tbl",
  },
);
assert.equal(
  bridgeFiles.get("ini:/work/ini-derived-session.ini"),
  "[EMC]\nMACHINE = xyzab-tdr\n",
);
assert.equal(
  loadedIniDerivedRandomSession.toolTable.result,
  "tooldata_load=0\nload_tool_path=/work/ini-derived-session-tool.tbl\nrandom_toolchanger=1\n",
);
assert.equal(
  loadedIniDerivedRandomSession.parameters.opfsPath,
  "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
);
assert.equal(
  loadedIniDerivedRandomSession.toolTable.opfsPath,
  "linuxcnc/machines/xyzab-tdr/tool.tbl",
);

await saveMachineTextFiles("ini-file-session", {
  ini: [
    "[EMC]",
    "MACHINE = ini-file-session",
    "",
    "[RS274NGC]",
    "PARAMETER_FILE = custom.var",
    "",
    "[EMCIO]",
    "TOOL_TABLE = custom-tool.tbl",
    "",
  ].join("\n"),
}, { storage });
await saveTextFile(
  "linuxcnc/machines/ini-file-session/custom.var",
  "5161 77.0\n5162 88.0\n",
  storage,
);
await saveTextFile(
  "linuxcnc/machines/ini-file-session/custom-tool.tbl",
  "T8 P8 Z4.5 D0.5\n",
  storage,
);
const loadedIniFileSession = await loadMachineSessionFromOpfs(
  bridgeInterp,
  "ini-file-session",
  {
    storage,
    iniSdk: bridgeIniSdk,
    iniWasmPath: "/work/ini-file-session.ini",
    parameterWasmPath: "/work/ini-file-session.var",
    toolTableWasmPath: "/work/ini-file-session-tool.tbl",
  },
);
assert.equal(
  loadedIniFileSession.parameters.opfsPath,
  "linuxcnc/machines/ini-file-session/custom.var",
);
assert.equal(
  loadedIniFileSession.toolTable.opfsPath,
  "linuxcnc/machines/ini-file-session/custom-tool.tbl",
);
assert.equal(bridgeFiles.get("/work/ini-file-session.var"), "5161 77.0\n5162 88.0\n");
assert.equal(bridgeFiles.get("/work/ini-file-session-tool.tbl"), "T8 P8 Z4.5 D0.5\n");
assert.equal(
  loadedIniFileSession.toolTable.result,
  "tooldata_load=0\nload_tool_path=/work/ini-file-session-tool.tbl\nrandom_toolchanger=0\n",
);

await saveMachineTextFiles("ini-override-session", {
  ini: [
    "[EMC]",
    "MACHINE = ini-override-session",
    "",
    "[RS274NGC]",
    "PARAMETER_FILE = ignored.var",
    "",
    "[EMCIO]",
    "TOOL_TABLE = ignored-tool.tbl",
    "",
  ].join("\n"),
}, { storage });
await saveTextFile(
  "linuxcnc/machines/ini-override-session/explicit.var",
  "5161 11.0\n5162 22.0\n",
  storage,
);
await saveTextFile(
  "linuxcnc/machines/ini-override-session/explicit-tool.tbl",
  "T3 P3 Z1.75 D0.375\n",
  storage,
);
const loadedIniOverrideSession = await loadMachineSessionFromOpfs(
  bridgeInterp,
  "ini-override-session",
  {
    storage,
    iniSdk: bridgeIniSdk,
    iniWasmPath: "/work/ini-override-session.ini",
    parameterFilename: "explicit.var",
    parameterWasmPath: "/work/ini-override-session.var",
    toolTableFilename: "explicit-tool.tbl",
    toolTableWasmPath: "/work/ini-override-session-tool.tbl",
  },
);
assert.equal(
  loadedIniOverrideSession.parameters.opfsPath,
  "linuxcnc/machines/ini-override-session/explicit.var",
);
assert.equal(
  loadedIniOverrideSession.toolTable.opfsPath,
  "linuxcnc/machines/ini-override-session/explicit-tool.tbl",
);
assert.equal(bridgeFiles.get("/work/ini-override-session.var"), "5161 11.0\n5162 22.0\n");
assert.equal(bridgeFiles.get("/work/ini-override-session-tool.tbl"), "T3 P3 Z1.75 D0.375\n");

await saveMachineTextFiles("invalid-ini-file-session", {
  ini: [
    "[EMC]",
    "MACHINE = invalid-ini-file-session",
    "",
    "[RS274NGC]",
    "PARAMETER_FILE = ../escape.var",
    "",
  ].join("\n"),
}, { storage });
await assert.rejects(
  () => loadMachineSessionFromOpfs(
    bridgeInterp,
    "invalid-ini-file-session",
    {
      storage,
      iniSdk: bridgeIniSdk,
      iniWasmPath: "/work/invalid-ini-file-session.ini",
    },
  ),
  /Invalid parameter filename/,
);

await saveMachineTextFiles("invalid-tool-file-session", {
  ini: [
    "[EMC]",
    "MACHINE = invalid-tool-file-session",
    "",
    "[EMCIO]",
    "TOOL_TABLE = nested/tool.tbl",
    "",
  ].join("\n"),
}, { storage });
await assert.rejects(
  () => loadMachineSessionFromOpfs(
    bridgeInterp,
    "invalid-tool-file-session",
    {
      storage,
      iniSdk: bridgeIniSdk,
      iniWasmPath: "/work/invalid-tool-file-session.ini",
    },
  ),
  /Invalid tool table filename/,
);

await assert.rejects(
  () => loadTextFile("linuxcnc/machines/missing.ini", storage),
  /missing file/,
);
await assert.rejects(
  () => saveTextFile("../escape.ini", "", storage),
  /Invalid OPFS path/,
);
assert.throws(
  () => machineIniPath("../escape"),
  /Invalid machine id/,
);
assert.throws(
  () => sessionSnapshotPath("session-1", "nested/snapshot.json"),
  /Invalid session snapshot filename/,
);
assert.throws(
  () => previewCachePath("fixture", "nested/preview.json"),
  /Invalid preview cache filename/,
);
assert.throws(
  () => gcodeProgramPath("../escape.ngc"),
  /Invalid G-code filename/,
);
await assert.rejects(
  () => saveGcodeProgram("nested/fixture.ngc", "", { storage }),
  /Invalid G-code filename/,
);
await assert.rejects(
  () => loadGcodeProgram("../escape.ngc", { storage }),
  /Invalid G-code filename/,
);
await assert.rejects(
  () => readMachineSessionReadiness("xyzab-tdr", {
    storage,
    gcodeOpfsPath: "../escape.ngc",
  }),
  /Invalid OPFS path/,
);
await assert.rejects(
  () => saveSessionSnapshot("session-1", {}, {
    storage,
    filename: "nested/snapshot.json",
  }),
  /Invalid session snapshot filename/,
);
await assert.rejects(
  () => loadSessionSnapshot("session-1", {
    storage,
    filename: "../snapshot.json",
  }),
  /Invalid session snapshot filename/,
);
await assert.rejects(
  () => saveMachineTextFiles("xyzab-tdr", null, { storage }),
  /machine files must be a plain object/,
);
await saveTextFile(sessionSnapshotPath("bad-json"), "{", storage);
await assert.rejects(
  () => loadSessionSnapshot("bad-json", { storage }),
  /Invalid session snapshot JSON/,
);
await saveTextFile(
  sessionSnapshotPath("bad-format"),
  JSON.stringify({ ...snapshot, sessionId: "bad-format", format: "other-format" }),
  storage,
);
await assert.rejects(
  () => loadSessionSnapshot("bad-format", { storage }),
  /Unsupported session snapshot format/,
);
await saveTextFile(
  sessionSnapshotPath("bad-version"),
  JSON.stringify({ ...snapshot, sessionId: "bad-version", version: 99 }),
  storage,
);
await assert.rejects(
  () => loadSessionSnapshot("bad-version", { storage }),
  /Unsupported session snapshot version/,
);
await saveTextFile(
  sessionSnapshotPath("bad-session-id"),
  JSON.stringify({ ...snapshot, sessionId: "other-session" }),
  storage,
);
await assert.rejects(
  () => loadSessionSnapshot("bad-session-id", { storage }),
  /Session snapshot id mismatch/,
);
await assert.rejects(
  () => loadSessionSnapshot("missing-session", { storage }),
  /missing directory|missing file/,
);
await assert.rejects(
  () => getOpfsRoot({}),
  /OPFS is not available/,
);

console.log("opfs_file_service_node_smoke=ok");
