import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
  mkdirSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import {
  createLocalTransport,
  LocalTransportError,
} from "../../../lib/fleet/transport-local.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: "~/fleet-transport-test.txt",
    ownership: "user",
    strategy: "copy",
    mode: "converge",
    content_digest: sha256Hex("hello"),
    owner: "fleet",
    ...overrides,
  };
}

function tempHome() {
  return mkdtempSync(join(tmpdir(), "fleet-transport-local-"));
}

test("read reports missing destination as exists:false without throwing", async () => {
  const home = tempHome();
  const transport = createLocalTransport({ home });
  const result = await transport.read("~/missing-file");
  assert.deepEqual(result, {
    exists: false,
    digest: null,
    ownership: "user",
    mode: null,
    linkTarget: null,
  });
});

test("fresh write lands content under synthetic HOME", async () => {
  const home = tempHome();
  const transport = createLocalTransport({ home });
  const content = Buffer.from("fleet local transport");
  const item = makeItem({
    destination: "~/bin/example.txt",
    content_digest: sha256Hex(content),
  });

  await transport.write(item, content);

  const dest = join(home, "bin/example.txt");
  assert.equal(readFileSync(dest, "utf8"), content.toString("utf8"));
  const readBack = await transport.read("~/bin/example.txt");
  assert.equal(readBack.exists, true);
  assert.equal(readBack.digest, item.content_digest);
  assert.equal(readBack.ownership, "user");
});

test("repeat write of matching content is a no-op", async () => {
  const home = tempHome();
  let renameCalls = 0;
  const transport = createLocalTransport({
    home,
    rename: async (from, to) => {
      renameCalls += 1;
      const { rename } = await import("node:fs/promises");
      return rename(from, to);
    },
  });
  const content = Buffer.from("idempotent payload");
  const item = makeItem({
    destination: "~/repeat.txt",
    content_digest: sha256Hex(content),
  });

  await transport.write(item, content);
  const dest = join(home, "repeat.txt");
  const mtimeAfterFirst = statSync(dest).mtimeMs;

  await transport.write(item, content);

  assert.equal(renameCalls, 1);
  assert.equal(statSync(dest).mtimeMs, mtimeAfterFirst);
  assert.equal(readFileSync(dest, "utf8"), content.toString("utf8"));
});

test("digest mismatch leaves prior content intact and removes staging path", async () => {
  const home = tempHome();
  const transport = createLocalTransport({ home });
  const original = Buffer.from("keep this version");
  const item = makeItem({
    destination: "~/protected.txt",
    content_digest: sha256Hex(original),
  });
  await transport.write(item, original);

  const dest = join(home, "protected.txt");
  const dir = join(home);
  const before = readdirSync(dir).sort();

  const badItem = makeItem({
    destination: "~/protected.txt",
    content_digest: sha256Hex("declared-but-wrong"),
  });
  await assert.rejects(
    () => transport.write(badItem, Buffer.from("different bytes")),
    (err) => err instanceof LocalTransportError && /digest mismatch/.test(err.message),
  );

  assert.equal(readFileSync(dest, "utf8"), original.toString("utf8"));
  const after = readdirSync(dir).sort();
  assert.deepEqual(after, before);
  assert.ok(!after.some((name) => name.includes(".staging-")));
});

test("root-ownership item throws before any filesystem mutation", async () => {
  const home = tempHome();
  const transport = createLocalTransport({ home });
  const item = makeItem({
    destination: "~/root-owned.txt",
    ownership: "root",
    content_digest: sha256Hex("root attempt"),
  });

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

  assert.equal(readdirSync(home).length, 0);
});

test("unitState queries systemctl --user via injectable exec", async () => {
  const calls = [];
  const transport = createLocalTransport({
    home: tempHome(),
    exec: async (argv) => {
      calls.push(argv);
      if (argv[2] === "is-enabled") {
        return { stdout: "enabled\n", stderr: "", code: 0 };
      }
      if (argv[2] === "is-active") {
        return { stdout: "inactive\n", stderr: "", code: 3 };
      }
      return { stdout: "", stderr: "unexpected", code: 1 };
    },
  });

  const state = await transport.unitState("example.service");
  assert.deepEqual(state, { enabled: true, active: false });
  assert.deepEqual(calls, [
    ["systemctl", "--user", "is-enabled", "example.service"],
    ["systemctl", "--user", "is-active", "example.service"],
  ]);
});

test("gitRef returns HEAD for paths inside a repo and null otherwise", async () => {
  const home = tempHome();
  const transport = createLocalTransport({
    home,
    exec: async (argv) => {
      if (argv[0] === "git" && argv.includes("rev-parse")) {
        const dir = argv[2];
        if (dir.endsWith("/in-repo")) {
          return { stdout: "abc123def456\n", stderr: "", code: 0 };
        }
        return { stdout: "", stderr: "not a git repository", code: 128 };
      }
      return { stdout: "", stderr: "unexpected", code: 1 };
    },
  });

  mkdirSync(join(home, "in-repo"), { recursive: true });
  writeFileSync(join(home, "in-repo", "file.txt"), "fixture\n");
  assert.equal(await transport.gitRef("~/in-repo/file.txt"), "abc123def456");
  assert.equal(await transport.gitRef("~/outside/file.txt"), null);
});

test("interrupted write after staging removes staging file and preserves destination", async () => {
  const home = tempHome();
  const setupTransport = createLocalTransport({ home });
  const original = Buffer.from("stable");
  const item = makeItem({
    destination: "~/interrupt.txt",
    content_digest: sha256Hex(original),
  });
  await setupTransport.write(item, original);

  const dest = join(home, "interrupt.txt");
  const next = makeItem({
    destination: "~/interrupt.txt",
    content_digest: sha256Hex("next version"),
  });

  const transport = createLocalTransport({
    home,
    rename: async () => {
      throw new Error("rename interrupted");
    },
  });
  await assert.rejects(() => transport.write(next, Buffer.from("next version")));

  assert.equal(readFileSync(dest, "utf8"), original.toString("utf8"));
  assert.ok(!readdirSync(join(home)).some((name) => name.includes(".staging-")));
});
