import test from "node:test";
import assert from "node:assert/strict";
import { execFile as execFileCallback } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";

import {
  buildStatusReport,
  claimIfNeeded,
  createShellBridge,
  discoverShellyDevices,
  finalizePendingUpdateOnStartup,
  getAgentStatus,
  getRelayStatus,
  loadConfig,
  parseCliArgs,
  processPolledShellAction,
  processPolledCommand,
  runCli,
  runUpdate,
  setRelayState,
  startAgent,
  handleAgentCommand,
  verifyPendingUpdate,
} from "../dist/agent.mjs";

const execFile = promisify(execFileCallback);
const agentEntryPath = fileURLToPath(new URL("../dist/agent.mjs", import.meta.url));

function makeFetchResponse(body) {
  const bytes = Buffer.from(body);
  return {
    ok: true,
    async arrayBuffer() {
      return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
    },
  };
}

function sha256Hex(body) {
  return createHash("sha256").update(body).digest("hex");
}

async function waitFor(predicate, { timeoutMs = 1000, intervalMs = 10, description = "condition" } = {}) {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    if (predicate()) {
      return;
    }

    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }

  throw new Error(`Timed out waiting for ${description}`);
}

test("claimIfNeeded persists claimed gateway credentials", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-"));
  const configPath = path.join(tempDir, "config.json");
  await writeFile(configPath, JSON.stringify({
    apiUrl: "https://api.example.test",
    installToken: "claim-token",
  }));

  const requests = [];
  const fakeFetch = async (url, options) => {
    requests.push({ url, options });
    return {
      ok: true,
      async json() {
        return {
          data: {
            gateway: { id: 9001 },
            agent_token: "agent-token",
            release_channel: "stable",
            broker_url: "https://broker.example.test",
          },
        };
      },
    };
  };

  const config = await claimIfNeeded({
    apiUrl: "https://api.example.test",
    installToken: "claim-token",
  }, configPath, fakeFetch);

  assert.equal(requests.length, 1);
  assert.equal(config.gatewayId, 9001);
  assert.equal(config.agentToken, "agent-token");
  assert.equal(config.brokerUrl, "https://broker.example.test");

  await rm(tempDir, { recursive: true, force: true });
});

test("relay status and switch commands support both Shelly RPC and legacy endpoints", async () => {
  const fakeFetch = async (url) => {
    if (String(url).includes("Switch.GetStatus")) {
      return {
        ok: true,
        async json() {
          return { output: true };
        },
      };
    }
    if (String(url).includes("Switch.Set")) {
      return {
        ok: true,
        async json() {
          return { output: false };
        },
      };
    }
    throw new Error(`Unexpected URL: ${url}`);
  };

  const status = await getRelayStatus({ localIp: "10.1.0.31", channel: 0 }, fakeFetch);
  const switched = await setRelayState({ localIp: "10.1.0.31", channel: 0, on: false }, fakeFetch);

  assert.equal(status.online, true);
  assert.equal(status.on, true);
  assert.equal(switched.on, false);
});

test("batch relay commands return per-relay results without failing the whole batch", async () => {
  const fakeFetch = async (url) => {
    const value = String(url);
    if (value.includes("10.1.0.31")) {
      return {
        ok: true,
        async json() {
          return { output: true };
        },
      };
    }
    throw new Error("relay offline");
  };

  const result = await handleAgentCommand({
    commandType: "BATCH_SET_RELAY_STATE",
    payload: {
      batch_id: "batch-1",
      commands: [
        { target: "MACHINE", relayId: "relay-machine", localIp: "10.1.0.31", channel: 0, on: true },
        { target: "EXIT", relayId: "relay-out", localIp: "10.1.0.32", channel: 0, on: true },
      ],
    },
  }, { fetchImpl: fakeFetch });

  assert.equal(result.batch_id, "batch-1");
  assert.equal(result.results.length, 2);
  assert.equal(result.results[0].ok, true);
  assert.equal(result.results[0].target, "MACHINE");
  assert.equal(result.results[1].ok, false);
  assert.match(result.results[1].error, /relay offline/);
});

test("Shelly discovery infers Gen3 from S3 relay model codes when generation is omitted", async () => {
  const inventory = await discoverShellyDevices({ candidateIps: ["192.168.1.2"] }, async (url) => {
    assert.equal(String(url), "http://192.168.1.2/shelly");
    return {
      ok: true,
      async json() {
        return {
          id: "shelly1minig3-e4b3231f6410",
          mac: "E4B3231F6410",
          model: "S3SW-001X8EU",
          type: "Shelly 1 Mini Gen3",
          num_switches: 1,
        };
      },
    };
  });

  assert.equal(inventory.length, 1);
  assert.equal(inventory[0].device_id, "E4B3231F6410");
  assert.equal(inventory[0].model, "S3SW-001X8EU");
  assert.equal(inventory[0].capabilities.generation, 3);
});

test("relay status reads use the legacy endpoint when the RPC status endpoint stalls", async () => {
  const urls = [];
  const fakeFetch = async (url) => {
    urls.push(String(url));
    if (String(url).includes("Switch.GetStatus")) {
      return new Promise(() => {});
    }

    if (String(url) === "http://10.1.0.31/relay/0") {
      return {
        ok: true,
        async json() {
          return { ison: false };
        },
      };
    }

    throw new Error(`Unexpected URL: ${url}`);
  };
  const startedAt = Date.now();

  const status = await getRelayStatus({ localIp: "10.1.0.31", channel: 0, timeoutMs: 50 }, fakeFetch);

  assert.equal(status.online, true);
  assert.equal(status.on, false);
  assert.deepEqual(urls.sort(), [
    "http://10.1.0.31/relay/0",
    "http://10.1.0.31/rpc/Switch.GetStatus?id=0",
  ]);
  assert.ok(Date.now() - startedAt < 250);
});

test("relay switch commands fail quickly when the local Shelly request stalls", async () => {
  let calls = 0;
  const hangingFetch = async () => {
    calls += 1;
    return new Promise(() => {});
  };
  const startedAt = Date.now();

  await assert.rejects(
    () => setRelayState({ localIp: "10.1.0.31", channel: 0, on: true, timeoutMs: 50 }, hangingFetch),
    /HTTP request timed out after 50ms/
  );

  assert.equal(calls, 1);
  assert.ok(Date.now() - startedAt < 500);
});

test("relay switch commands pass timer values to local Shelly APIs", async () => {
  const legacyUrls = [];
  const legacyFetch = async (url) => {
    legacyUrls.push(String(url));
    return {
      ok: true,
      async json() {
        return { ison: true, has_timer: true, timer_duration: 3 };
      },
    };
  };

  await setRelayState({
    localIp: "10.1.0.31",
    channel: 0,
    on: true,
    toggleAfter: 3,
    deviceGeneration: 1,
  }, legacyFetch);

  assert.equal(
    legacyUrls[0],
    "http://10.1.0.31/relay/0?turn=on&timer=3"
  );

  const gen3Urls = [];
  const gen3Fetch = async (url) => {
    gen3Urls.push(String(url));

    return {
      ok: true,
      async json() {
        return { output: true };
      },
    };
  };

  await setRelayState({
    localIp: "10.1.0.31",
    channel: 0,
    on: true,
    toggle_after: 3,
    device_generation: 3,
  }, gen3Fetch);

  assert.equal(
    gen3Urls[0],
    "http://10.1.0.31/rpc/Switch.Set?id=0&on=true&toggle_after=3"
  );

  const fallbackUrls = [];
  const legacyFallbackFetch = async (url) => {
    fallbackUrls.push(String(url));
    if (String(url).includes("/rpc/")) {
      throw new Error("RPC switch endpoint unsupported");
    }

    return {
      ok: true,
      async json() {
        return { ison: true, has_timer: true, timer_duration: 3 };
      },
    };
  };

  await setRelayState({
    localIp: "10.1.0.31",
    channel: 0,
    on: true,
    toggle_after: 3,
    device_model: "S3SW-001X8EU",
  }, legacyFallbackFetch);

  assert.deepEqual(fallbackUrls, [
    "http://10.1.0.31/rpc/Switch.Set?id=0&on=true&toggle_after=3",
    "http://10.1.0.31/relay/0?turn=on&timer=3",
  ]);

  const cappedUrls = [];
  const cappedFetch = async (url) => {
    cappedUrls.push(String(url));

    return {
      ok: true,
      async json() {
        return { output: true };
      },
    };
  };

  await setRelayState({
    localIp: "10.1.0.31",
    channel: 0,
    on: true,
    toggle_after: 999999999,
    device_generation: 3,
  }, cappedFetch);

  assert.equal(
    cappedUrls[0],
    "http://10.1.0.31/rpc/Switch.Set?id=0&on=true&toggle_after=5"
  );
});

test("runUpdate stages a pending verification restart after installing new artifacts", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-update-"));
  const configPath = path.join(tempDir, "config.json");
  const liveConfig = {
    apiUrl: "https://api.example.test",
    gatewayId: 42,
    agentToken: "agent-token",
    installDir: tempDir,
    restartMode: "spawn",
    installedVersion: "1.0.0",
    targetVersion: "1.0.0",
  };

  await writeFile(configPath, JSON.stringify(liveConfig, null, 2));
  await writeFile(path.join(tempDir, "agent.mjs"), "// old agent\n");
  await writeFile(path.join(tempDir, "package.json"), JSON.stringify({ name: "old-edge-agent" }, null, 2));

  const execCalls = [];
  const fakeExecFile = async (command, args, options) => {
    execCalls.push({ command, args, options });
    return { stdout: "{}" };
  };
  const agentBody = "// new agent\n";
  const packageBody = JSON.stringify({ name: "new-edge-agent" }, null, 2);
  const fakeFetch = async (url) => {
    if (String(url).endsWith("/agent.mjs")) {
      return makeFetchResponse(agentBody);
    }
    if (String(url).endsWith("/package.json")) {
      return makeFetchResponse(packageBody);
    }
    throw new Error(`Unexpected URL: ${url}`);
  };

  const result = await runUpdate({
    artifactUrl: "https://api.example.test/edge-agent/artifacts/agent.mjs",
    sha256: sha256Hex(agentBody),
    packageUrl: "https://api.example.test/edge-agent/artifacts/package.json",
    packageSha256: sha256Hex(packageBody),
    targetVersion: "1.1.0",
    releaseChannel: "stable",
    restartMode: "spawn",
  }, fakeFetch, {
    configPath,
    config: liveConfig,
    liveConfig,
    execFileImpl: fakeExecFile,
  });

  const persistedConfig = JSON.parse(await readFile(configPath, "utf8"));

  assert.equal(result.__agentCommandEnvelope, true);
  assert.equal(result.payload.verification_pending, true);
  assert.equal(result.payload.target_version, "1.1.0");
  assert.equal(liveConfig.targetVersion, "1.1.0");
  assert.equal(liveConfig.installedVersion, "1.0.0");
  assert.equal(persistedConfig.pendingUpdate.targetVersion, "1.1.0");
  assert.equal(persistedConfig.pendingUpdate.previousVersion, "1.0.0");
  assert.match(persistedConfig.pendingUpdate.backupDir, /\.updates/);
  assert.equal(await readFile(path.join(tempDir, "agent.mjs"), "utf8"), "// new agent\n");
  assert.equal(execCalls.length, 2);
  assert.deepEqual(execCalls[0].args, ["install", "--omit=dev"]);
  assert.deepEqual(execCalls[1].args, [path.join(tempDir, "agent.mjs"), "status", "--config", configPath]);

  await rm(tempDir, { recursive: true, force: true });
});
test("runUpdate rejects artifacts without required checksums", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-update-checksum-"));
  const configPath = path.join(tempDir, "config.json");
  const liveConfig = {
    apiUrl: "https://api.example.test",
    gatewayId: 42,
    agentToken: "agent-token",
    installDir: tempDir,
    restartMode: "spawn",
    installedVersion: "1.0.0",
    targetVersion: "1.0.0",
  };

  await writeFile(configPath, JSON.stringify(liveConfig, null, 2));
  await writeFile(path.join(tempDir, "agent.mjs"), "// old agent\n");

  let fetchCalled = false;
  await assert.rejects(
    runUpdate({
      artifactUrl: "https://api.example.test/edge-agent/artifacts/agent.mjs",
      targetVersion: "1.1.0",
    }, async () => {
      fetchCalled = true;
      return makeFetchResponse("// new agent\n");
    }, {
      configPath,
      config: liveConfig,
      liveConfig,
      execFileImpl: async () => ({ stdout: "{}" }),
    }),
    /Agent artifact checksum is required/
  );

  assert.equal(fetchCalled, false);
  assert.equal(await readFile(path.join(tempDir, "agent.mjs"), "utf8"), "// old agent\n");

  await rm(tempDir, { recursive: true, force: true });
});


test("handleAgentCommand returns an uninstall follow-up envelope for gateway removal", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-uninstall-envelope-"));
  const configPath = path.join(tempDir, "config.json");
  await writeFile(configPath, JSON.stringify({
    apiUrl: "https://api.example.test",
    gatewayId: 42,
    agentToken: "agent-token",
    installToken: "install-token",
    brokerUrl: "https://broker.example.test",
    restartMode: "spawn",
  }));

  const result = await handleAgentCommand(
    {
      commandType: "UNINSTALL_AGENT",
      payload: {
        serviceName: "truckwash-edge-agent.service",
      },
    },
    {
      configPath,
      config: await loadConfig(configPath),
      liveConfig: null,
    }
  );

  assert.equal(result.__agentCommandEnvelope, true);
  assert.equal(result.payload.uninstall_scheduled, true);
  assert.equal(result.followUp.type, "UNINSTALL_AGENT");
  assert.equal(result.followUp.uninstallPlan.configPath, configPath);
  assert.equal(result.followUp.uninstallPlan.serviceName, "truckwash-edge-agent.service");

  await rm(tempDir, { recursive: true, force: true });
});

test("processPolledCommand acknowledges uninstall before clearing credentials and exiting", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-uninstall-"));
  const configPath = path.join(tempDir, "config.json");
  const config = {
    apiUrl: "https://api.example.test",
    gatewayId: 42,
    agentToken: "agent-token",
    installToken: "install-token",
    brokerUrl: "https://broker.example.test",
    restartMode: "spawn",
  };
  await writeFile(configPath, JSON.stringify(config, null, 2));

  const commandResultPosts = [];
  const fakeFetch = async (url, options = {}) => {
    const body = options.body ? JSON.parse(options.body) : {};

    if (String(url).endsWith("/commands/77/result")) {
      commandResultPosts.push({ url, body });
      return {
        ok: true,
        async json() {
          return { data: { acknowledged: true } };
        },
      };
    }

    throw new Error(`Unexpected URL: ${url}`);
  };

  const exitCodes = [];
  const result = await processPolledCommand(
    {
      ...config,
      configPath,
    },
    {
      id: 77,
      commandType: "UNINSTALL_AGENT",
      payload: {
        serviceName: "truckwash-edge-agent.service",
      },
    },
    fakeFetch,
    {
      waitImpl: async () => {},
      exitProcessImpl: (code) => {
        exitCodes.push(code);
      },
    }
  );

  const persisted = JSON.parse(await readFile(configPath, "utf8"));

  assert.equal(result.ok, true);
  assert.equal(result.payload.uninstall_scheduled, true);
  assert.equal(commandResultPosts.length, 1);
  assert.equal(commandResultPosts[0].body.ok, true);
  assert.equal(commandResultPosts[0].body.payload.uninstall_scheduled, true);
  assert.deepEqual(exitCodes, [0]);
  assert.equal(persisted.gatewayId, null);
  assert.equal(persisted.agentToken, null);
  assert.equal(persisted.installToken, null);
  assert.equal(persisted.brokerUrl, null);
  assert.equal(persisted.lastUninstall.state, "SCHEDULED");

  await rm(tempDir, { recursive: true, force: true });
});

test("finalizePendingUpdateOnStartup promotes the target version and clears the pending update marker", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-startup-"));
  const configPath = path.join(tempDir, "config.json");
  const config = {
    apiUrl: "https://api.example.test",
    gatewayId: 42,
    agentToken: "agent-token",
    installDir: tempDir,
    installedVersion: "1.0.0",
    targetVersion: "1.1.0",
    pendingUpdate: {
      targetVersion: "1.1.0",
      previousVersion: "1.0.0",
      releaseChannel: "stable",
      backupDir: path.join(tempDir, ".updates", "backup"),
    },
  };
  await writeFile(configPath, JSON.stringify(config, null, 2));

  const finalized = await finalizePendingUpdateOnStartup(config, configPath, {
    liveConfig: config,
  });
  const persisted = JSON.parse(await readFile(configPath, "utf8"));

  assert.equal(finalized.installedVersion, "1.1.0");
  assert.equal(finalized.pendingUpdate, null);
  assert.equal(finalized.lastUpdate.state, "COMPLETED");
  assert.equal(persisted.installedVersion, "1.1.0");
  assert.equal(persisted.pendingUpdate, null);
  assert.equal(persisted.lastUpdate.targetVersion, "1.1.0");

  await rm(tempDir, { recursive: true, force: true });
});

test("verifyPendingUpdate rolls back the previous files and respawns the agent when startup verification times out", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-rollback-"));
  const updatesDir = path.join(tempDir, ".updates", "rollback-case");
  const configPath = path.join(tempDir, "config.json");

  await mkdir(updatesDir, { recursive: true });
  await writeFile(path.join(tempDir, "agent.mjs"), "// broken new agent\n");
  await writeFile(path.join(tempDir, "package.json"), JSON.stringify({ name: "broken-edge-agent" }, null, 2));
  await writeFile(path.join(updatesDir, "agent.mjs"), "// old agent\n");
  await writeFile(path.join(updatesDir, "package.json"), JSON.stringify({ name: "old-edge-agent" }, null, 2));

  const config = {
    apiUrl: "https://api.example.test",
    gatewayId: 42,
    agentToken: "agent-token",
    installDir: tempDir,
    restartMode: "spawn",
    installedVersion: "1.0.0",
    targetVersion: "1.1.0",
    pendingUpdate: {
      targetVersion: "1.1.0",
      previousVersion: "1.0.0",
      releaseChannel: "stable",
      backupDir: updatesDir,
      installDir: tempDir,
      restartMode: "spawn",
      verificationTimeoutSeconds: 5,
    },
  };
  await writeFile(configPath, JSON.stringify(config, null, 2));

  const execCalls = [];
  const fakeExecFile = async (command, args, options) => {
    execCalls.push({ command, args, options });
    return { stdout: "{}" };
  };
  const spawnCalls = [];
  const fakeSpawn = (command, args, options) => {
    spawnCalls.push({ command, args, options });
    return {
      unref() {},
    };
  };

  const result = await verifyPendingUpdate(configPath, {
    execFileImpl: fakeExecFile,
    spawnImpl: fakeSpawn,
    timeoutMs: 1,
    waitImpl: async () => {},
    verifyIntervalMs: 1,
  });

  const persisted = JSON.parse(await readFile(configPath, "utf8"));

  assert.equal(result.verified, false);
  assert.equal(result.rolledBack, true);
  assert.equal(await readFile(path.join(tempDir, "agent.mjs"), "utf8"), "// old agent\n");
  assert.equal(persisted.pendingUpdate, null);
  assert.equal(persisted.lastUpdate.state, "ROLLED_BACK");
  assert.equal(persisted.installedVersion, "1.0.0");
  assert.equal(execCalls.length, 1);
  assert.deepEqual(execCalls[0].args, ["install", "--omit=dev"]);
  assert.equal(spawnCalls.length, 1);
  assert.equal(spawnCalls[0].command, process.execPath);
  assert.deepEqual(spawnCalls[0].args, [path.join(tempDir, "agent.mjs"), "--config", configPath]);

  await rm(tempDir, { recursive: true, force: true });
});

test("shell bridge proxies PTY output, input, resize, close, and dispose events", async () => {
  const messages = [];
  const createdPtys = [];

  const shell = createShellBridge(
    (message) => messages.push(message),
    {
      createPtyProcess: async (options) => {
        const listeners = {
          data: null,
          exit: null,
        };

        const pty = {
          options,
          writes: [],
          resizes: [],
          killed: false,
          onData(callback) {
            listeners.data = callback;
            return {
              dispose() {
                listeners.data = null;
              },
            };
          },
          onExit(callback) {
            listeners.exit = callback;
            return {
              dispose() {
                listeners.exit = null;
              },
            };
          },
          write(data) {
            this.writes.push(data);
          },
          resize(cols, rows) {
            this.resizes.push({ cols, rows });
          },
          kill() {
            this.killed = true;
            listeners.exit?.({ exitCode: 0 });
          },
          emitData(data) {
            listeners.data?.(data);
          },
        };

        createdPtys.push(pty);
        return pty;
      },
    }
  );

  await shell.open({
    sessionId: "test-shell",
    shellCommand: "/bin/bash",
    shellArgs: ["-l"],
    cols: 90,
    rows: 24,
    cwd: "/tmp",
  });

  createdPtys[0].emitData("root@pi:~# ");
  shell.input({ sessionId: "test-shell", data: "ls\r" });
  shell.resize({ sessionId: "test-shell", cols: 120, rows: 40 });
  shell.dispose();

  assert.equal(createdPtys.length, 1);
  assert.equal(createdPtys[0].options.command, "/bin/bash");
  assert.deepEqual(createdPtys[0].options.args, ["-l"]);
  assert.equal(createdPtys[0].options.cols, 90);
  assert.equal(createdPtys[0].options.rows, 24);
  assert.equal(createdPtys[0].options.cwd, "/tmp");
  assert.deepEqual(createdPtys[0].writes, ["ls\r"]);
  assert.deepEqual(createdPtys[0].resizes, [{ cols: 120, rows: 40 }]);
  assert.equal(createdPtys[0].killed, true);
  assert.ok(messages.some((message) => message.type === "SHELL_OPENED" && message.sessionId === "test-shell"));
  assert.ok(messages.some((message) => message.type === "SHELL_OUTPUT" && message.data.includes("root@pi")));
  assert.ok(messages.some((message) => message.type === "SHELL_EXIT" && message.code === 0));
});

test("shell bridge reports structured spawn failures", async () => {
  const messages = [];
  const shell = createShellBridge(
    (message) => messages.push(message),
    {
      createPtyProcess: async () => {
        throw new Error("node-pty unavailable");
      },
    }
  );

  await shell.open({ sessionId: "spawn-failure-shell" });

  assert.ok(
    messages.some(
      (message) =>
        message.type === "SHELL_OUTPUT" &&
        message.sessionId === "spawn-failure-shell" &&
        /Failed to start root shell: node-pty unavailable/.test(message.data)
    )
  );
  assert.ok(
    messages.some(
      (message) =>
        message.type === "SHELL_EXIT" &&
        message.sessionId === "spawn-failure-shell" &&
        message.code === 1 &&
        message.reason === "shell_spawn_failed" &&
        message.message === "node-pty unavailable"
    )
  );
});

test("startAgent reports API polling metadata, executes polled commands, and uploads shell events", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-"));
  const configPath = path.join(tempDir, "config.json");
  await writeFile(configPath, JSON.stringify({
    apiUrl: "https://api.example.test",
    gatewayId: 42,
    agentToken: "agent-token",
    heartbeatIntervalSeconds: 0.05,
    commandPollTimeoutSeconds: 0,
    commandPollRetryDelayMs: 5,
    shellActionPollTimeoutSeconds: 0,
    shellActionPollRetryDelayMs: 5,
    enableShellAccess: true,
  }));

  const heartbeats = [];
  const commandResultPosts = [];
  const shellActionResults = [];
  const shellEventPosts = [];
  let polledCommandDelivered = false;
  let openActionDelivered = false;
  let closeActionDelivered = false;
  let metricSample = 0;

  const fakeFetch = async (url, options = {}) => {
    const body = options.body ? JSON.parse(options.body) : {};

    if (String(url).endsWith("/heartbeat")) {
      heartbeats.push({ url, body });
      return {
        ok: true,
        async json() {
          return { data: { ok: true } };
        },
      };
    }

    if (String(url).endsWith("/commands/poll")) {
      if (!polledCommandDelivered) {
        polledCommandDelivered = true;
        return {
          ok: true,
          async json() {
            return {
              data: {
                id: 99,
                commandType: "DISCOVER_SHELLY",
                payload: {
                  candidateIps: ["10.1.0.31"],
                },
              },
            };
          },
        };
      }

      await new Promise((resolve) => setTimeout(resolve, 5));
      return {
        ok: true,
        async json() {
          return { data: null };
        },
      };
    }

    if (String(url).endsWith("/commands/99/result")) {
      commandResultPosts.push({ url, body });
      return {
        ok: true,
        async json() {
          return { data: { acknowledged: true } };
        },
      };
    }

    if (String(url).endsWith("/shell-actions/poll")) {
      if (!openActionDelivered) {
        openActionDelivered = true;
        return {
          ok: true,
          async json() {
            return {
              data: {
                id: 501,
                actionType: "OPEN",
                payload: {
                  sessionId: 44,
                  cols: 100,
                  rows: 30,
                  reason: "Investigate relay drift",
                },
              },
            };
          },
        };
      }

      if (!closeActionDelivered) {
        closeActionDelivered = true;
        return {
          ok: true,
          async json() {
            return {
              data: {
                id: 502,
                actionType: "CLOSE",
                payload: {
                  sessionId: 44,
                },
              },
            };
          },
        };
      }

      await new Promise((resolve) => setTimeout(resolve, 5));
      return {
        ok: true,
        async json() {
          return { data: null };
        },
      };
    }

    if (/\/shell-actions\/\d+\/result$/.test(String(url))) {
      shellActionResults.push({ url, body });
      return {
        ok: true,
        async json() {
          return { data: { acknowledged: true } };
        },
      };
    }

    if (String(url).includes("/shell-sessions/44/events")) {
      shellEventPosts.push({ url, body });
      return {
        ok: true,
        async json() {
          return { data: { acknowledged: true } };
        },
      };
    }

    if (String(url) === "http://10.1.0.31/shelly") {
      return {
        ok: true,
        async json() {
          return {
            mac: "AA:BB:CC:DD:EE:FF",
            model: "Shelly Plus 1PM",
            num_switches: 1,
          };
        },
      };
    }

    throw new Error(`Unexpected URL: ${url}`);
  };

  const collectMetricsImpl = async ({ latencyMs }) => {
    metricSample += 1;
    return {
      cpuSnapshot: { idle: metricSample, total: metricSample + 10 },
      metrics: {
        latency_ms: latencyMs,
        cpu_usage_pct: 27.5,
        memory_usage_pct: 48.2,
        memory_used_bytes: 2048,
        memory_total_bytes: 4096,
        disk_usage_pct: 61.4,
        disk_used_bytes: 8192,
        disk_total_bytes: 16384,
        disk_mount: "/",
      },
    };
  };

  const shellCalls = [];
  const createShellBridgeImpl = (sendMessage) => ({
    async open(payload) {
      shellCalls.push({ type: "open", payload });
      sendMessage({ type: "SHELL_OPENED", sessionId: payload.sessionId });
      sendMessage({ type: "SHELL_OUTPUT", sessionId: payload.sessionId, data: "root@pi:~# " });
    },
    input(payload) {
      shellCalls.push({ type: "input", payload });
    },
    resize(payload) {
      shellCalls.push({ type: "resize", payload });
    },
    close(payload) {
      shellCalls.push({ type: "close", payload });
      sendMessage({ type: "SHELL_EXIT", sessionId: payload.sessionId, code: 0 });
    },
    dispose() {
      shellCalls.push({ type: "dispose" });
    },
  });

  let agent = null;
  try {
    agent = await startAgent({
      configPath,
      fetchImpl: fakeFetch,
      collectMetricsImpl,
      createShellBridgeImpl,
    });

    assert.equal(heartbeats[0].body.status, "ONLINE");
    assert.equal(heartbeats[0].body.metadata.command_transport, "API_POLLING");
    assert.equal(heartbeats[0].body.metadata.shell_transport, "API_POLLING");
    assert.equal(heartbeats[0].body.metadata.system_metrics.cpu_usage_pct, 27.5);
    assert.equal(heartbeats[0].body.metadata.system_metrics.memory_usage_pct, 48.2);
    assert.equal(heartbeats[0].body.metadata.system_metrics.disk_usage_pct, 61.4);
    assert.equal(heartbeats[0].body.metadata.system_metrics.latency_ms, null);
    assert.equal(heartbeats[0].body.metadata.broker_connected, false);
    assert.equal(heartbeats[0].body.metadata.broker_url, null);
    assert.equal("discovery_status" in heartbeats[0].body, false);

    await waitFor(
      () =>
        commandResultPosts.length === 1 &&
        shellActionResults.length === 2 &&
        shellEventPosts.length > 0 &&
        heartbeats.some(
          (heartbeat) =>
            heartbeat.body.metadata.system_metrics &&
            typeof heartbeat.body.metadata.system_metrics.latency_ms === "number"
        ),
      { timeoutMs: 1000, description: "agent polling activity and follow-up heartbeat" }
    );

    assert.equal(commandResultPosts.length, 1);
    assert.equal(commandResultPosts[0].body.ok, true);
    assert.equal(Array.isArray(commandResultPosts[0].body.payload.inventory), true);
    assert.equal(commandResultPosts[0].body.payload.inventory[0].device_id, "AA:BB:CC:DD:EE:FF");
    assert.equal(commandResultPosts[0].body.payload.inventory[0].capabilities.generation, 2);

    assert.ok(shellCalls.some((call) => call.type === "open"));
    assert.ok(shellCalls.some((call) => call.type === "close"));
    assert.equal(shellActionResults.length, 2);
    assert.ok(shellActionResults.every((result) => result.body.ok === true));
    assert.ok(
      shellEventPosts.some((request) =>
        request.body.events.some((event) => event.type === "OPENED")
      )
    );
    assert.ok(
      shellEventPosts.some((request) =>
        request.body.events.some((event) => event.type === "OUTPUT")
      )
    );
    assert.ok(
      shellEventPosts.some((request) =>
        request.body.events.some((event) => event.type === "CLOSED")
      )
    );
    assert.ok(
      heartbeats.some(
        (heartbeat) =>
          heartbeat.body.metadata.system_metrics &&
          typeof heartbeat.body.metadata.system_metrics.latency_ms === "number"
      )
    );
  } finally {
    await agent?.stop();
    await rm(tempDir, { recursive: true, force: true });
  }
});

test("processPolledShellAction denies shell access when locally disabled", async () => {
  const submissions = [];
  const fakeFetch = async (url, options = {}) => {
    if (/\/shell-actions\/\d+\/result$/.test(String(url))) {
      submissions.push({ url, body: JSON.parse(options.body) });
      return {
        ok: true,
        async json() {
          return { data: { acknowledged: true } };
        },
      };
    }

    throw new Error(`Unexpected URL: ${url}`);
  };

  const shell = {
    async open() {
      throw new Error("should not run");
    },
    input() {
      throw new Error("should not run");
    },
    resize() {
      throw new Error("should not run");
    },
    close() {
      throw new Error("should not run");
    },
  };

  const result = await processPolledShellAction(
    {
      apiUrl: "https://api.example.test",
      gatewayId: 42,
      agentToken: "agent-token",
      enableShellAccess: false,
    },
    {
      id: 501,
      actionType: "OPEN",
      payload: {
        sessionId: 44,
      },
    },
    shell,
    fakeFetch
  );

  assert.equal(result.ok, false);
  assert.match(result.error, /Shell access is disabled/);
  assert.equal(submissions.length, 1);
  assert.equal(submissions[0].body.ok, false);
});

test("status helpers report config without exposing the agent token", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-status-"));
  const configPath = path.join(tempDir, "config.json");
  await writeFile(configPath, JSON.stringify({
    apiUrl: "https://api.example.test",
    installToken: "install-token",
    gatewayId: 42,
    agentToken: "agent-token",
    installedVersion: "1.2.3",
    targetVersion: "1.2.4",
    releaseChannel: "stable",
    brokerUrl: "https://broker.example.test",
    heartbeatIntervalSeconds: 30,
  }));

  const report = await getAgentStatus(configPath);
  const built = buildStatusReport(configPath, {
    apiUrl: "https://api.example.test",
    installToken: "install-token",
    gatewayId: 42,
    agentToken: "agent-token",
    brokerUrl: "https://broker.example.test",
  });

  assert.equal(report.command, "status");
  assert.equal(report.configPath, configPath);
  assert.equal(report.claimed, true);
  assert.equal(report.state, "CLAIMED");
  assert.equal(report.gatewayId, 42);
  assert.equal(report.transport, "HYBRID");
  assert.equal(report.brokerUrl, "https://broker.example.test");
  assert.equal(report.hasInstallToken, true);
  assert.equal("agentToken" in report, false);
  assert.equal(built.state, "CLAIMED");
  assert.equal(built.transport, "HYBRID");
  assert.equal(built.brokerUrl, "https://broker.example.test");
  assert.equal("agentToken" in built, false);

  await rm(tempDir, { recursive: true, force: true });
});

test("parseCliArgs understands the status command and config flag", () => {
  assert.deepEqual(
    parseCliArgs(["status", "--config", "/tmp/config.json"]),
    { command: "status", configPath: "/tmp/config.json" }
  );
  assert.deepEqual(
    parseCliArgs(["--config", "/tmp/config.json"]),
    { command: "start", configPath: "/tmp/config.json" }
  );
});

test("runCli emits a status report for the status command", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-cli-"));
  const configPath = path.join(tempDir, "config.json");
  await writeFile(configPath, JSON.stringify({
    apiUrl: "https://api.example.test",
    installToken: "install-token",
  }));

  let stdout = "";
  const originalWrite = process.stdout.write.bind(process.stdout);
  process.stdout.write = ((chunk, encoding, callback) => {
    stdout += String(chunk);
    if (typeof encoding === "function") {
      encoding();
    } else if (typeof callback === "function") {
      callback();
    }
    return true;
  });

  try {
    const report = await runCli(["status", "--config", configPath]);
    assert.equal(report.state, "PENDING_CLAIM");
    const parsed = JSON.parse(stdout);
    assert.equal(parsed.command, "status");
    assert.equal(parsed.claimed, false);
    assert.equal(parsed.transport, "API_POLLING");
    assert.equal("brokerWsUrl" in parsed, false);
  } finally {
    process.stdout.write = originalWrite;
  }

  await rm(tempDir, { recursive: true, force: true });
});

test("agent CLI status command prints the report", async () => {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-process-"));
  const configPath = path.join(tempDir, "config.json");
  await writeFile(configPath, JSON.stringify({
    apiUrl: "https://api.example.test",
    installToken: "install-token",
    gatewayId: 7,
    agentToken: "secret-token",
  }));

  const { stdout } = await execFile(
    process.execPath,
    [agentEntryPath, "status", "--config", configPath],
    { windowsHide: true, encoding: "utf8" }
  );
  const parsed = JSON.parse(stdout);

  assert.equal(parsed.command, "status");
  assert.equal(parsed.gatewayId, 7);
  assert.equal(parsed.claimed, true);
  assert.equal(parsed.transport, "API_POLLING");
  assert.equal("agentToken" in parsed, false);

  await rm(tempDir, { recursive: true, force: true });
});
