import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import { parseRegistry } from "../../workstation/claude/lib/buildbox-registry.mjs";
import {
  createSshTransport,
  SshTransportError,
} from "../../../lib/fleet/transport-ssh.mjs";

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

/**
 * @param {Partial<import("../../../lib/fleet/types.mjs").DesiredItem>} overrides
 */
function makeItem(overrides = {}) {
  return {
    id: "test:item",
    source: { kind: "repo", path: "modules/example", ref: null },
    destination: "/home/user/fleet-transport-test.txt",
    ownership: "user",
    strategy: "copy",
    mode: "converge",
    content_digest: sha256Hex("hello"),
    owner: "fleet",
    ...overrides,
  };
}

const access = (addr) => ({
  lan: null,
  tailscale_ip: {
    host: `100.64.0.${addr}`,
    port: 2222,
    user: "user",
    identity_file: "~/.ssh/id_ed25519_buildbox",
  },
  tailscale_ssh: null,
});

const host = (name, state, addr = 1) => ({
  name,
  ssh_alias: name,
  state,
  machine_id: `${addr}`.repeat(32).slice(0, 32),
  roles: ["builder"],
  access: access(addr),
  rustdesk: null,
  notes: `${name} fixture`,
});

const FIXTURE = {
  schema_version: 1,
  hosts: [
    host("debian1", "reachable", 1),
    host("debian2", "unreachable", 2),
  ],
  orders: { build: ["debian1"] },
};

function fixtureRegistry(patch = {}) {
  return parseRegistry(JSON.stringify({ ...FIXTURE, ...patch }), "fixture");
}

/**
 * @param {string} text
 */
function parseShqArgs(text) {
  const args = [];
  let current = "";
  let quote = null;
  for (let i = 0; i < text.length; i += 1) {
    const ch = text[i];
    if (quote === "'") {
      if (ch === "'") {
        if (text[i + 1] === "'") {
          current += "'";
          i += 1;
        } else {
          quote = null;
        }
      } else {
        current += ch;
      }
      continue;
    }
    if (ch === "'") {
      quote = "'";
      continue;
    }
    if (ch === " ") {
      if (current) {
        args.push(current);
        current = "";
      }
      continue;
    }
    current += ch;
  }
  if (current) args.push(current);
  return args;
}

/**
 * @param {string} command
 */
function decodeBashInvocation(command) {
  const match = /^printf %s '([^']*)' \| base64 -d \| bash -s (.*)$/.exec(command);
  if (!match) return null;
  const script = Buffer.from(match[1], "base64").toString("utf8");
  return { script, args: parseShqArgs(match[2]) };
}

class FakeRemote {
  constructor() {
    /** @type {Map<string, Buffer>} */
    this.files = new Map();
    /** @type {string[]} */
    this.history = [];
    /** @type {((context: { command: string, args: string[] }) => void|{ stdout?: string, stderr?: string, code?: number, throw?: Error })|null} */
    this.onPromote = null;
    /** @type {Map<string, { enabled: boolean, active: boolean }>} */
    this.units = new Map();
  }

  /**
   * @param {string} path
   */
  readDigest(path) {
    if (!this.files.has(path)) {
      return { absent: true };
    }
    const content = this.files.get(path);
    return {
      absent: false,
      digest: sha256Hex(content),
      ownership: "user",
      mode: "644",
      linkTarget: null,
    };
  }

  /**
   * @param {string} script
   * @param {string[]} args
   */
  runBash(script, args) {
    if (script.includes("dest=$1") && script.includes("printf 'promoted")) {
      const [dest, staging, expected] = args;
      if (this.onPromote) {
        const hook = this.onPromote({ command: "promote", args });
        if (hook?.throw) throw hook.throw;
        if (hook) return hook;
      }
      if (this.files.has(dest)) {
        const current = sha256Hex(this.files.get(dest));
        if (current === expected) {
          this.files.delete(staging);
          return { stdout: "noop\n", stderr: "", code: 0 };
        }
      }
      if (!this.files.has(staging)) {
        return { stdout: "", stderr: "missing staging", code: 1 };
      }
      const actual = sha256Hex(this.files.get(staging));
      if (actual !== expected) {
        this.files.delete(staging);
        return { stdout: "", stderr: "digest mismatch", code: 42 };
      }
      this.files.set(dest, this.files.get(staging));
      this.files.delete(staging);
      return { stdout: "promoted\n", stderr: "", code: 0 };
    }

    if (script.includes("__FLEET__:absent")) {
      const [path] = args;
      const info = this.readDigest(path);
      if (info.absent) {
        return { stdout: "__FLEET__:absent\n", stderr: "", code: 0 };
      }
      return {
        stdout: `exists=1\ndigest=${info.digest}\nownership=${info.ownership}\nmode=${info.mode}\nlink=\n`,
        stderr: "",
        code: 0,
      };
    }

    if (script.includes('rm -f "$1"')) {
      const [path] = args;
      this.files.delete(path);
      return { stdout: "", stderr: "", code: 0 };
    }

    return { stdout: "", stderr: `unhandled bash script:\n${script}`, code: 127 };
  }

  /**
   * @param {string} command
   */
  runSsh(command) {
    this.history.push(command);
    const bash = decodeBashInvocation(command);
    if (bash) {
      return this.runBash(bash.script, bash.args);
    }
    const mkdir = /^mkdir -p '([^']*)'$/.exec(command);
    if (mkdir) {
      return { stdout: "", stderr: "", code: 0 };
    }
    const enabled = /^systemctl --user is-enabled '([^']+)'$/.exec(command);
    if (enabled) {
      const unit = this.units.get(enabled[1]) ?? { enabled: false, active: false };
      return unit.enabled
        ? { stdout: "enabled\n", stderr: "", code: 0 }
        : { stdout: "disabled\n", stderr: "", code: 1 };
    }
    const active = /^systemctl --user is-active '([^']+)'$/.exec(command);
    if (active) {
      const unit = this.units.get(active[1]) ?? { enabled: false, active: false };
      return unit.active
        ? { stdout: "active\n", stderr: "", code: 0 }
        : { stdout: "inactive\n", stderr: "", code: 3 };
    }
    if (command.includes("git -C")) {
      return { stdout: "", stderr: "not a git repository", code: 0 };
    }
    return { stdout: "", stderr: `unhandled ssh command: ${command}`, code: 127 };
  }

  /**
   * @param {string[]} argv
   */
  exec(argv) {
    if (argv[0] === "ssh") {
      const command = argv[argv.length - 1];
      try {
        return Promise.resolve(this.runSsh(command));
      } catch (err) {
        return Promise.reject(err);
      }
    }
    if (argv[0] === "scp") {
      const localPath = argv[argv.length - 2];
      const remoteSpec = argv[argv.length - 1];
      const remotePath = remoteSpec.split(":", 2)[1];
      this.files.set(remotePath, readFileSync(localPath));
      return Promise.resolve({ stdout: "", stderr: "", code: 0 });
    }
    return Promise.resolve({ stdout: "", stderr: `unhandled argv: ${argv.join(" ")}`, code: 127 });
  }
}

test("read reports missing destination as exists:false without throwing", async () => {
  const remote = new FakeRemote();
  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: (argv) => remote.exec(argv),
  });
  const result = await transport.read("/home/user/missing-file");
  assert.deepEqual(result, {
    exists: false,
    digest: null,
    ownership: "user",
    mode: null,
    linkTarget: null,
  });
});

test("fresh write lands content on the remote node", async () => {
  const remote = new FakeRemote();
  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: (argv) => remote.exec(argv),
  });
  const content = Buffer.from("fleet ssh transport");
  const item = makeItem({
    destination: "/home/user/bin/example.txt",
    content_digest: sha256Hex(content),
  });

  await transport.write(item, content);

  assert.equal(remote.files.get(item.destination)?.toString("utf8"), content.toString("utf8"));
  assert.ok(remote.history.some((cmd) => cmd.startsWith("mkdir -p ")));
  assert.ok(remote.history.some((cmd) => decodeBashInvocation(cmd)?.script.includes("printf 'promoted")));
});

test("repeat write of matching content is a no-op", async () => {
  const remote = new FakeRemote();
  let promoteCalls = 0;
  remote.onPromote = () => {
    promoteCalls += 1;
  };
  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: (argv) => remote.exec(argv),
  });
  const content = Buffer.from("idempotent payload");
  const item = makeItem({
    destination: "/home/user/repeat.txt",
    content_digest: sha256Hex(content),
  });

  await transport.write(item, content);
  assert.equal(promoteCalls, 1);

  await transport.write(item, content);

  assert.equal(promoteCalls, 1);
  assert.equal(remote.files.get(item.destination)?.toString("utf8"), content.toString("utf8"));
});

test("interrupted promote removes staging and preserves prior content", async () => {
  const remote = new FakeRemote();
  const original = Buffer.from("stable");
  const item = makeItem({
    destination: "/home/user/interrupt.txt",
    content_digest: sha256Hex(original),
  });
  remote.files.set(item.destination, Buffer.from(original));

  const next = makeItem({
    destination: item.destination,
    content_digest: sha256Hex("next version"),
  });

  remote.onPromote = ({ args }) => {
    const staging = args[1];
    remote.files.set(staging, Buffer.from("next version"));
    return { throw: new Error("promote interrupted") };
  };

  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: (argv) => remote.exec(argv),
  });

  await assert.rejects(() => transport.write(next, Buffer.from("next version")));

  assert.equal(remote.files.get(item.destination)?.toString("utf8"), original.toString("utf8"));
  assert.ok(![...remote.files.keys()].some((path) => path.includes(".staging-")));
});

test("root-ownership item throws before any remote mutation", async () => {
  const remote = new FakeRemote();
  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: (argv) => remote.exec(argv),
  });
  const item = makeItem({
    destination: "/home/user/root-owned.txt",
    ownership: "root",
    content_digest: sha256Hex("root attempt"),
  });

  await assert.rejects(
    () => transport.write(item, Buffer.from("root attempt")),
    (err) => err instanceof SshTransportError && /refuses root-owned/.test(err.message),
  );

  assert.equal(remote.files.size, 0);
  assert.equal(remote.history.length, 0);
});

test("unreachable registry host surfaces a node-named transport error", async () => {
  assert.throws(
    () =>
      createSshTransport({
        hostRef: "debian2",
        registry: fixtureRegistry(),
        exec: async () => ({ stdout: "", stderr: "", code: 0 }),
      }),
    (err) => err instanceof SshTransportError && /debian2/.test(err.message) && /unreachable/.test(err.message),
  );
});

test("connection failure surfaces a node-named transport error", async () => {
  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: async () => ({
      stdout: "",
      stderr: "ssh: connect to host 100.64.0.1 port 2222: Connection refused",
      code: 255,
    }),
  });

  await assert.rejects(
    () => transport.read("/home/user/anything"),
    (err) =>
      err instanceof SshTransportError
      && /debian1/.test(err.message)
      && /Connection refused/.test(err.message),
  );
});

test("unitState queries systemctl --user over ssh", async () => {
  const remote = new FakeRemote();
  remote.units.set("example.service", { enabled: true, active: false });
  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: (argv) => remote.exec(argv),
  });

  const state = await transport.unitState("example.service");
  assert.deepEqual(state, { enabled: true, active: false });
  assert.ok(remote.history.some((cmd) => cmd.includes("systemctl --user is-enabled")));
  assert.ok(remote.history.some((cmd) => cmd.includes("systemctl --user is-active")));
});

test("digest mismatch leaves prior content intact and removes staging path", async () => {
  const remote = new FakeRemote();
  const original = Buffer.from("keep this version");
  const item = makeItem({
    destination: "/home/user/protected.txt",
    content_digest: sha256Hex(original),
  });
  remote.files.set(item.destination, Buffer.from(original));

  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: (argv) => remote.exec(argv),
  });

  const badItem = makeItem({
    destination: item.destination,
    content_digest: sha256Hex("declared-but-wrong"),
  });

  await assert.rejects(
    () => transport.write(badItem, Buffer.from("different bytes")),
    (err) => err instanceof SshTransportError && /digest mismatch/.test(err.message),
  );

  assert.equal(remote.files.get(item.destination)?.toString("utf8"), original.toString("utf8"));
  assert.ok(![...remote.files.keys()].some((path) => path.includes(".staging-")));
});

test("real scp upload path is used for remote writes", async () => {
  const remote = new FakeRemote();
  const scpCalls = [];
  const transport = createSshTransport({
    hostRef: "debian1",
    registry: fixtureRegistry(),
    exec: async (argv) => {
      if (argv[0] === "scp") scpCalls.push(argv);
      return remote.exec(argv);
    },
  });
  const content = Buffer.from("uploaded through scp");
  const item = makeItem({
    destination: "/home/user/scp-target.txt",
    content_digest: sha256Hex(content),
  });

  await transport.write(item, content);

  assert.equal(scpCalls.length, 1);
  assert.equal(scpCalls[0][0], "scp");
  assert.match(scpCalls[0].at(-1), /user@100\.64\.0\.1:\/home\/user\/\.scp-target\.txt\.staging-/);
});
