import { execFile } from "node:child_process";
import { copyFile, chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { beforeEach, afterEach, describe, expect, it } from "vitest";
import { sanitizedGitEnv } from "./git/gitEnv.js";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
const dockerEntrypoint = join(repoRoot, "docker", "pi-web-docker");

let tempDir = "";

interface CommandResult {
  stdout: string;
  stderr: string;
}

interface FakeDocker {
  binDir: string;
  logPath: string;
}

beforeEach(async () => {
  tempDir = await mkdtemp(join(tmpdir(), "pi-web-docker-test-"));
});

afterEach(async () => {
  await rm(tempDir, { recursive: true, force: true });
});

describe("Docker command assets", () => {
  // The Docker control shell scripts intentionally support Linux and macOS hosts.
  // Keep Windows CI on static/syntax coverage instead of executing POSIX host-path and socket flows there.
  const dockerCommandIt = it.skipIf(process.platform === "win32");

  it("keeps shell entrypoints syntactically valid", async () => {
    await Promise.all([
      execUtf8("sh", ["-n", dockerEntrypoint], process.env),
      execUtf8("sh", ["-n", join(repoRoot, "docker", "install.sh")], process.env),
      execUtf8("sh", ["-n", join(repoRoot, "docker", "internal", "dev", "compose")], process.env),
      execUtf8("bash", ["-n", join(repoRoot, "docker", "internal", "dev", "sync-node-modules")], process.env),
      execUtf8("sh", ["-n", join(repoRoot, "docker", "internal", "host-profile.sh")], process.env),
    ]);
  });

  it("packages the canonical Docker command and internal support assets", async () => {
    const [dockerfile, devDockerfile, runtimeCompose, devCompose, installer, devWrapper, dependencySync, dockerignore] = await Promise.all([
      readRepoFile("docker/Dockerfile"),
      readRepoFile("docker/Dockerfile.dev"),
      readRepoFile("docker/compose.yml"),
      readRepoFile("docker/compose.dev.yml"),
      readRepoFile("docker/install.sh"),
      readRepoFile("docker/internal/dev/compose"),
      readRepoFile("docker/internal/dev/sync-node-modules"),
      readRepoFile("docker/.dockerignore"),
    ]);
    const customImageHooksIndex = devDockerfile.indexOf("for script in /tmp/pi-web-custom-image.d/*.sh");
    const dependencyGenerationIndex = devDockerfile.indexOf("/opt/pi-web-dev-dependencies/generation");

    expect(dockerfile).toContain("COPY pi-web-docker /usr/local/bin/pi-web-docker");
    expect(dockerfile).toContain("COPY internal/bin/hostexec /usr/local/bin/hostexec");
    expect(dockerfile).toContain("COPY internal/image/install-opensuse-base /usr/local/sbin/install-pi-web-opensuse-base");
    expect(dockerfile).toContain("--include=peer");
    expect(dockerfile).toContain('"@jmfederico/pi-web@${PI_WEB_VERSION}" --allow-scripts=node-pty');
    expect(dockerfile).toContain('peer_pi_bin="${global_root}/@jmfederico/pi-web/node_modules/.bin/pi"');
    expect(dockerfile).not.toContain("@earendil-works/pi-coding-agent@");
    expect(devDockerfile).toContain("COPY docker/pi-web-docker /usr/local/bin/pi-web-docker");
    expect(devDockerfile).toContain("COPY docker/internal/bin/hostexec /usr/local/bin/hostexec");
    expect(devDockerfile).toContain("COPY --chmod=0755 docker/internal/dev/sync-node-modules /usr/local/sbin/pi-web-dev-sync-node-modules");
    expect(devDockerfile).toContain("/opt/pi-web-dev-dependencies/node_modules");
    // Hooks can mutate the dependency seed, so its cache generation must be finalized afterward.
    expect(customImageHooksIndex).toBeGreaterThanOrEqual(0);
    expect(dependencyGenerationIndex).toBeGreaterThan(customImageHooksIndex);
    expect(dependencySync).toContain(".pi-web-dev-dependency-generation");
    expect(dockerignore).toContain("!pi-web-docker");
    expect(dockerignore).toContain("!internal/bin/hostexec");
    expect(installer).toContain("write_asset pi-web-docker 0755");
    expect(installer).toContain("write_asset internal/host-profile.sh 0644");
    expect(installer).toContain("compose_cmd --project-name \"$compose_project_name\"");
    expect(installer).toContain("PI_WEB_DOCKER_INSTALL_DIR=$install_dir");
    expect(installer).toContain("PI_WEB_DOCKER_REF=$asset_ref");
    expect(installer).toContain("COMPOSE_PROJECT_NAME=$compose_project_name");
    expect(devWrapper).toContain("$repo_root/docker/internal/host-profile.sh");
    expect(devWrapper).toContain("--project-name \"$compose_project_name\"");
    expect(devWrapper).toContain("PI_WEB_DOCKER_DEV_REPO_ROOT=$repo_root");
    expect(devWrapper).toContain("COMPOSE_PROJECT_NAME=$compose_project_name");
    expect(runtimeCompose).toContain("PI_WEB_DOCKER_RUNTIME: \"1\"");
    expect(runtimeCompose).toContain("PI_WEB_DOCKER_MODE: runtime");
    expect(runtimeCompose).toContain("PI_WEB_DOCKER_INSTALL_DIR: ${PI_WEB_DOCKER_INSTALL_DIR:?set by docker/install.sh}");
    expect(runtimeCompose).toContain("PI_WEB_DOCKER_HELPER_IMAGE: ${PI_WEB_IMAGE:-pi-web:local}");
    expect(runtimeCompose).toContain("COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-pi-web}");
    expect(devCompose).toContain("PI_WEB_DOCKER_MODE: dev");
    expect(devCompose).toContain("PI_WEB_DOCKER_DEV_REPO_ROOT: ${PI_WEB_DOCKER_DEV_REPO_ROOT:?set by docker/pi-web-docker --dev}");
    expect(devCompose).toContain("PI_WEB_DOCKER_HELPER_IMAGE: ${PI_WEB_DEV_IMAGE:-pi-web:dev}");
    expect(devCompose).toContain("COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-pi-web-dev}");
    expect(devCompose).toContain("/usr/local/sbin/pi-web-dev-sync-node-modules");
    expect(devCompose.match(/volumes: \*pi-web-dev-volumes/g)).toHaveLength(3);
  });

  dockerCommandIt("fetches remote installer assets without clobbering the write target", async () => {
    const installDir = join(tempDir, "remote-runtime");
    const fakeDocker = await installFakeDocker();
    await installFakeCurl(fakeDocker.binDir);
    await installFakeUname(fakeDocker.binDir, "Darwin");
    const home = join(tempDir, "home");
    const socketPath = join(home, ".docker", "run", "docker.sock");

    await withUnixSocket(socketPath, async () => {
      await execUtf8("sh", [
        join(repoRoot, "docker", "install.sh"),
        "--install-dir", installDir,
        "--data-dir", join(installDir, "data"),
        "--asset-ref", "test-assets",
        "--skip-compose",
      ], {
        ...cleanProcessEnv(),
        PATH: `${fakeDocker.binDir}:${process.env["PATH"] ?? ""}`,
        HOME: home,
        FAKE_DOCKER_LOG: fakeDocker.logPath,
        PI_WEB_DOCKER_ASSET_BASE: "https://assets.example.test/docker",
      });
    });

    expect(await readFile(join(installDir, "Dockerfile"), "utf8")).toContain("COPY pi-web-docker /usr/local/bin/pi-web-docker");
    expect(await readFile(join(installDir, "pi-web-docker"), "utf8")).toContain("Usage: pi-web-docker");
    const env = await readFile(join(installDir, ".env"), "utf8");
    expect(env).toContain(`PI_WEB_DOCKER_INSTALL_DIR=${installDir}`);
    expect(env).toContain("PI_WEB_DOCKER_REF=test-assets");
  });

  dockerCommandIt("runs status through Docker Compose in the foreground", async () => {
    const installDir = await createRuntimeInstall();
    const fakeDocker = await installFakeDocker();

    const result = await runDockerCommand(["status"], runtimeEnv(fakeDocker, installDir));

    expect(result.stdout).toContain("fake docker compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml ps");
    const log = await readFile(fakeDocker.logPath, "utf8");
    expect(log).toContain("compose version");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml ps");
    expect(log).not.toContain("run -d");
  });

  dockerCommandIt("runs production host lifecycle commands through the generated runtime env", async () => {
    const installDir = await createRuntimeInstall();
    const fakeDocker = await installFakeDocker();
    const env = runtimeHostEnv(fakeDocker, installDir);

    await runDockerCommand(["start"], env);
    await runDockerCommand(["stop"], env);
    await runDockerCommand(["restart-sessiond"], env);
    await runDockerCommand(["logs", "web"], env);
    await runDockerCommand(["shell", "sessiond"], env);
    await runDockerCommand(["cli", "config", "show"], env);

    const log = await readFile(fakeDocker.logPath, "utf8");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml up -d");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml down");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml restart sessiond");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml logs -f web");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml exec sessiond bash");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml exec web pi-web config show");
    expect(log).not.toContain("run -d");
  });

  dockerCommandIt("ignores ambient Compose project names for runtime lifecycle commands", async () => {
    const installDir = await createRuntimeInstall();
    const fakeDocker = await installFakeDocker();

    await runDockerCommand(["status"], {
      ...runtimeHostEnv(fakeDocker, installDir),
      COMPOSE_PROJECT_NAME: "ambient-project",
    });

    const log = await readFile(fakeDocker.logPath, "utf8");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml ps");
    expect(log).not.toContain("--project-name ambient-project");
  });

  dockerCommandIt("runs development commands through generated env while preserving host user ids", async () => {
    const devRoot = await createDevRepoFixture();
    const fakeDocker = await installFakeDocker();
    await installFakeUname(fakeDocker.binDir, "Darwin");
    await installFakeId(fakeDocker.binDir, 1234, 2345);
    const home = join(tempDir, "home");
    const runtimeDataDir = join(tempDir, "runtime-data");
    const runtimeEnvFile = join(tempDir, "runtime.env");
    const socketPath = join(home, ".docker", "run", "docker.sock");
    await writeFile(runtimeEnvFile, [
      "PI_WEB_UID=0",
      "PI_WEB_GID=0",
      `PI_WEB_DOCKER_DATA_DIR=${runtimeDataDir}`,
      "PI_WEB_BIND_ADDR=0.0.0.0",
      "COMPOSE_PROJECT_NAME=runtime-project",
      "",
    ].join("\n"));

    await withUnixSocket(socketPath, async () => {
      await runDockerCommand(["--dev", "status"], devHostEnv(fakeDocker, devRoot, home, { PI_WEB_DOCKER_RUNTIME_ENV_FILE: runtimeEnvFile }));
    });

    const generatedEnvPath = join(devRoot, ".pi-web", "docker-compose-dev.generated.env");
    const generatedEnv = await readFile(generatedEnvPath, "utf8");
    expect(generatedEnv).toContain("PI_WEB_UID=1234\n");
    expect(generatedEnv).toContain("PI_WEB_GID=2345\n");
    expect(generatedEnv).toContain("DOCKER_GID=0\n");
    expect(generatedEnv).toContain(`PI_WEB_DOCKER_DATA_DIR=${runtimeDataDir}\n`);
    expect(generatedEnv).toContain(`PI_WEB_DOCKER_DEV_REPO_ROOT=${devRoot}\n`);
    expect(generatedEnv).toContain("COMPOSE_PROJECT_NAME=pi-web-dev\n");
    expect(generatedEnv).toContain("PI_WEB_DEV_API_BIND_ADDR=0.0.0.0\n");
    expect(generatedEnv).not.toContain("COMPOSE_PROJECT_NAME=runtime-project");

    await withUnixSocket(socketPath, async () => {
      await runDockerCommand(["--dev", "status"], devHostEnv(fakeDocker, devRoot, home, {
        COMPOSE_PROJECT_NAME: "ambient-dev-project",
        PI_WEB_DOCKER_RUNTIME_ENV_FILE: runtimeEnvFile,
      }));
    });
    const regeneratedEnv = await readFile(generatedEnvPath, "utf8");
    expect(regeneratedEnv).toContain("COMPOSE_PROJECT_NAME=pi-web-dev\n");
    expect(regeneratedEnv).toContain(`PI_WEB_DOCKER_DATA_DIR=${runtimeDataDir}\n`);
    expect(regeneratedEnv).not.toContain("COMPOSE_PROJECT_NAME=ambient-dev-project");

    const localConfig = await readFile(join(devRoot, ".pi-web", "docker-compose-dev.local.env"), "utf8");
    expect(localConfig).toContain("docker/pi-web-docker --dev creates this file once");
    expect(localConfig).toContain("PI_WEB_UID and PI_WEB_GID default to the current host user");
    const override = await readFile(join(devRoot, ".pi-web", "docker-compose-dev.host.generated.yml"), "utf8");
    expect(override).toContain(socketPath);
    expect(override).toContain(devRoot);
    const log = await readFile(fakeDocker.logPath, "utf8");
    expect(log).toContain(`compose --project-name pi-web-dev --env-file ${generatedEnvPath} -f ${devRoot}/docker/compose.dev.yml -f ${devRoot}/.pi-web/docker-compose-dev.host.generated.yml ps`);
  });

  dockerCommandIt("rejects development commands as root unless explicitly allowed", async () => {
    const fakeDocker = await installFakeDocker();
    await installFakeId(fakeDocker.binDir, 0, 0);

    const result = await runDockerCommandAllowFailure(["--dev", "status"], {
      ...cleanProcessEnv(),
      PATH: `${fakeDocker.binDir}:${process.env["PATH"] ?? ""}`,
    });

    expect(result.exitCode).not.toBe(0);
    expect(result.stderr).toContain("refusing to run Docker development mode as root");
  });

  dockerCommandIt("passes the root override through to the development compose helper", async () => {
    const fakeDocker = await installFakeDocker();
    await installFakeId(fakeDocker.binDir, 0, 0);
    const helperLog = join(tempDir, "dev-helper.log");
    const devRoot = await createDevRepoFixtureWithFakeHelper(helperLog);

    await runDockerCommand(["--dev", "--allow-root", "status"], {
      ...cleanProcessEnv(),
      PATH: `${fakeDocker.binDir}:${process.env["PATH"] ?? ""}`,
      PI_WEB_DOCKER_DEV_REPO_ROOT: devRoot,
    });

    expect(await readFile(helperLog, "utf8")).toBe("allow=1 args=ps\n");
  });

  dockerCommandIt("refuses development updates when the checkout has uncommitted files", async () => {
    const helperLog = join(tempDir, "dev-helper.log");
    const devRoot = await createCleanDevGitRepoWithFakeHelper(helperLog);
    const fakeDocker = await installFakeDocker();
    await installFakeId(fakeDocker.binDir, 1234, 2345);
    await writeFile(join(devRoot, "staged.txt"), "changed\n", "utf8");
    await execUtf8("git", ["-C", devRoot, "add", "staged.txt"], cleanProcessEnv());
    await writeFile(join(devRoot, "modified.txt"), "changed\n", "utf8");
    await writeFile(join(devRoot, "untracked.txt"), "untracked\n", "utf8");

    const result = await runDockerCommandAllowFailure(
      ["--dev", "update"],
      devHostEnv(fakeDocker, devRoot, join(tempDir, "home")),
    );

    expect(result.exitCode).not.toBe(0);
    expect(result.stderr).toContain("refusing to update the Docker development stack because the checkout has uncommitted changes");
    expect(result.stderr).toContain("staged.txt");
    expect(result.stderr).toContain("modified.txt");
    expect(result.stderr).toContain("?? untracked.txt");
    expect(result.stderr).toContain("commit, stash, or remove these changes");
    await expect(readFile(helperLog, "utf8")).rejects.toThrow();
  });

  dockerCommandIt("refuses dirty development updates before scheduling a detached helper", async () => {
    const helperLog = join(tempDir, "dev-helper.log");
    const devRoot = await createCleanDevGitRepoWithFakeHelper(helperLog);
    const fakeDocker = await installFakeDocker();
    await installFakeId(fakeDocker.binDir, 1234, 2345);
    await writeFile(join(devRoot, "untracked.txt"), "untracked\n", "utf8");

    const result = await runDockerCommandAllowFailure(["--dev", "update"], devRuntimeEnv(fakeDocker, devRoot));

    expect(result.exitCode).not.toBe(0);
    expect(result.stderr).toContain("checkout has uncommitted changes");
    expect(result.stdout).not.toContain("Started detached PI WEB Docker helper");
    await expect(readFile(fakeDocker.logPath, "utf8")).rejects.toThrow();
    await expect(readFile(helperLog, "utf8")).rejects.toThrow();
  });

  dockerCommandIt("refuses development updates while a Git operation is in progress", async () => {
    const helperLog = join(tempDir, "dev-helper.log");
    const devRoot = await createCleanDevGitRepoWithFakeHelper(helperLog);
    const fakeDocker = await installFakeDocker();
    await installFakeId(fakeDocker.binDir, 1234, 2345);
    const head = (await execUtf8("git", ["-C", devRoot, "rev-parse", "HEAD"], cleanProcessEnv())).stdout.trim();
    await writeFile(join(devRoot, ".git", "MERGE_HEAD"), `${head}\n`, "utf8");

    const result = await runDockerCommandAllowFailure(
      ["--dev", "update"],
      devHostEnv(fakeDocker, devRoot, join(tempDir, "home")),
    );

    expect(result.exitCode).not.toBe(0);
    expect(result.stderr).toContain("while a Git merge is in progress");
    expect(result.stderr).toContain("resolve or abort the Git merge");
    await expect(readFile(helperLog, "utf8")).rejects.toThrow();
  });

  dockerCommandIt("allows clean development updates and dirty development starts", async () => {
    const helperLog = join(tempDir, "dev-helper.log");
    const devRoot = await createCleanDevGitRepoWithFakeHelper(helperLog);
    const fakeDocker = await installFakeDocker();
    await installFakeId(fakeDocker.binDir, 1234, 2345);
    const env = devHostEnv(fakeDocker, devRoot, join(tempDir, "home"));

    await runDockerCommand(["--dev", "update"], env);
    await writeFile(join(devRoot, "in-progress-work.txt"), "dirty by design\n", "utf8");
    await runDockerCommand(["--dev", "start"], env);

    expect(await readFile(helperLog, "utf8")).toBe([
      "allow=0 args=build --pull",
      "allow=0 args=up -d --force-recreate --remove-orphans",
      "allow=0 args=up -d --build",
      "",
    ].join("\n"));
  });

  dockerCommandIt("starts development detached helpers as the generated dev user", async () => {
    const devRoot = await createDevGeneratedEnv({ uid: 1234, gid: 2345, dockerGid: 3456 });
    const fakeDocker = await installFakeDocker();
    await installFakeId(fakeDocker.binDir, 1234, 2345);

    await runDockerCommand(["--dev", "restart-sessiond"], devRuntimeEnv(fakeDocker, devRoot));

    const log = await readFile(fakeDocker.logPath, "utf8");
    expect(log).toContain(`--env-file ${devRoot}/.pi-web/docker-compose-dev.generated.env`);
    expect(log).toContain("--group-add 3456");
    expect(log).toContain("--user 1234:2345");
    expect(log).toContain("PI_WEB_DOCKER_MODE=dev");
    expect(log).toContain("PI_WEB_DOCKER_ALLOW_ROOT=0");
    expect(log).toContain("PI_WEB_DOCKER_HELPER_IMAGE=pi-web:test");
    expect(log).toContain(`PI_WEB_DOCKER_DEV_REPO_ROOT=${devRoot}`);
    expect(log).toContain("COMPOSE_PROJECT_NAME=pi-web-dev-test");
    expect(log).toContain("pi-web.docker-helper.mode=dev");
    expect(log).toContain("pi-web:test pi-web-docker --dev __run-detached restart-sessiond");
    expect(log).not.toContain("--user 0:0");
  });

  dockerCommandIt("rejects inside-container commands whose explicit mode does not match the container mode", async () => {
    const result = await runDockerCommandAllowFailure(["restart-sessiond"], {
      ...cleanProcessEnv(),
      PI_WEB_DOCKER_RUNTIME: "1",
      PI_WEB_DOCKER_MODE: "dev",
    });

    expect(result.exitCode).not.toBe(0);
    expect(result.stderr).toContain("this PI WEB Docker container is in dev mode");
  });

  dockerCommandIt("routes production install to the bootstrap installer", async () => {
    const result = await runDockerCommand(["install", "--help"], cleanProcessEnv());

    expect(result.stdout).toContain("Usage: docker/install.sh [options]");
  });

  dockerCommandIt("explains source checkout runtime-mode mistakes", async () => {
    const fakeDocker = await installFakeDocker();

    const result = await runDockerCommandAllowFailure(["start"], {
      ...cleanProcessEnv(),
      PATH: `${fakeDocker.binDir}:${process.env["PATH"] ?? ""}`,
      HOME: "/home/pi-web-test",
    });

    expect(result.exitCode).not.toBe(0);
    expect(result.stderr).toContain("runtime install assets were not found");
    expect(result.stderr).toContain("running this checkout's Docker command in runtime mode");
    expect(result.stderr).toContain("./docker/pi-web-docker --dev start");
    expect(result.stderr).toContain("/home/pi-web-test/.local/share/pi-web-docker/pi-web-docker start");
    expect(result.stderr).toContain("PI_WEB_DOCKER_INSTALL_DIR");
  });

  dockerCommandIt("starts restart-sessiond in a detached Docker helper", async () => {
    const installDir = await createRuntimeInstall();
    const fakeDocker = await installFakeDocker();

    const result = await runDockerCommand(["restart-sessiond"], runtimeEnv(fakeDocker, installDir));

    expect(result.stdout).toContain("Started detached PI WEB Docker helper");
    expect(result.stdout).toContain("Streaming detached PI WEB Docker helper logs inline.");
    expect(result.stdout).toContain("Reconnect with: docker logs -f pi-web-docker-restart-sessiond-");
    expect(result.stdout).toContain("fake helper log");
    const log = await readFile(fakeDocker.logPath, "utf8");
    expect(log).toContain("container inspect");
    expect(log).toContain("run -d");
    expect(log).toContain("--env-file");
    expect(log).toContain(`${installDir}/.env`);
    expect(log).toContain("--volumes-from");
    expect(log).toContain("--group-add 3456");
    expect(log).toContain("--user 1234:2345");
    expect(log).toContain(`PI_WEB_DOCKER_INSTALL_DIR=${installDir}`);
    expect(log).toContain(`PI_WEB_DOCKER_DATA_DIR=${join(installDir, "data")}`);
    expect(log).toContain("PI_WEB_PORT=12345");
    expect(log).toContain("PI_WEB_DOCKER_EXTRA_HOST_PATHS=/srv/pi-web-extra /opt/pi-web-extra");
    expect(log).toContain("PI_WEB_EXTRA_ZYPPER_PACKAGES=git-lfs jq");
    expect(log).not.toContain('PI_WEB_EXTRA_ZYPPER_PACKAGES="git-lfs jq"');
    expect(log).toContain("PI_WEB_DOCKER_HELPER_IMAGE=pi-web:test");
    expect(log).toContain("COMPOSE_PROJECT_NAME=pi-web-test");
    expect(log).toContain("pi-web.docker-helper.mode=runtime");
    expect(log).toContain("pi-web.docker-helper.root=");
    expect(log).toContain("pi-web.docker-helper.project=pi-web-test");
    expect(log).toContain("pi-web:test pi-web-docker __run-detached restart-sessiond");
    expect(log).not.toContain("--user 0:0");
    expect(log).not.toContain("compose -f compose.yml -f compose.override.yml restart sessiond");
  });

  dockerCommandIt("executes the detached restart-sessiond action through Compose", async () => {
    const installDir = await createRuntimeInstall();
    const fakeDocker = await installFakeDocker();

    await runDockerCommand(["__run-detached", "restart-sessiond"], runtimeEnv(fakeDocker, installDir));

    const log = await readFile(fakeDocker.logPath, "utf8");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml restart sessiond");
    expect(log).not.toContain("run -d");
  });

  dockerCommandIt("executes the detached runtime update action through Compose without nesting helpers", async () => {
    const installDir = await createRuntimeInstall();
    const fakeDocker = await installFakeDocker();

    await runDockerCommand(["__run-detached", "update"], runtimeEnv(fakeDocker, installDir));

    const log = await readFile(fakeDocker.logPath, "utf8");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml build --pull --no-cache");
    expect(log).toContain("compose --project-name pi-web-test --env-file .env -f compose.yml -f compose.override.yml up -d --force-recreate --remove-orphans");
    expect(log).not.toContain("run -d");
  });
});

async function readRepoFile(relativePath: string): Promise<string> {
  return await readFile(join(repoRoot, relativePath), "utf8");
}

function runDockerCommand(args: string[], env: NodeJS.ProcessEnv): Promise<CommandResult> {
  return execUtf8("sh", [dockerEntrypoint, ...args], env);
}

function runDockerCommandAllowFailure(args: string[], env: NodeJS.ProcessEnv): Promise<CommandResult & { exitCode: number }> {
  return execUtf8AllowFailure("sh", [dockerEntrypoint, ...args], env);
}

function execUtf8(file: string, args: string[], env: NodeJS.ProcessEnv): Promise<CommandResult> {
  return new Promise((resolvePromise, reject) => {
    execFile(file, args, { encoding: "utf8", env }, (error, stdout, stderr) => {
      if (error !== null) {
        reject(error instanceof Error ? error : new Error("Process failed"));
        return;
      }
      resolvePromise({ stdout, stderr });
    });
  });
}

function execUtf8AllowFailure(file: string, args: string[], env: NodeJS.ProcessEnv): Promise<CommandResult & { exitCode: number }> {
  return new Promise((resolvePromise) => {
    execFile(file, args, { encoding: "utf8", env }, (error, stdout, stderr) => {
      const exitCode = typeof error === "object" && error !== null && "code" in error && typeof error.code === "number" ? error.code : 0;
      resolvePromise({ stdout, stderr, exitCode });
    });
  });
}

async function createRuntimeInstall(): Promise<string> {
  const installDir = join(tempDir, "runtime");
  await mkdir(installDir, { recursive: true });
  await writeFile(join(installDir, ".env"), [
    "PI_WEB_UID=1234",
    "PI_WEB_GID=2345",
    "DOCKER_GID=3456",
    `PI_WEB_DOCKER_DATA_DIR=${join(installDir, "data")}`,
    `PI_WEB_DOCKER_INSTALL_DIR=${installDir}`,
    "PI_WEB_DOCKER_EXTRA_HOST_PATHS=\"/srv/pi-web-extra /opt/pi-web-extra\"",
    "PI_WEB_BIND_ADDR=127.0.0.1",
    "PI_WEB_PORT=12345",
    "PI_WEB_EXTRA_ZYPPER_PACKAGES=\"git-lfs jq\"",
    "PI_WEB_IMAGE=pi-web:test",
    "COMPOSE_PROJECT_NAME=pi-web-test",
    "",
  ].join("\n"), "utf8");
  await writeFile(join(installDir, "compose.yml"), "name: pi-web\nservices: {}\n", "utf8");
  await writeFile(join(installDir, "compose.override.yml"), "services: {}\n", "utf8");
  return installDir;
}

async function createDevRepoFixture(): Promise<string> {
  const devRoot = join(tempDir, "dev-repo");
  await mkdir(join(devRoot, "docker", "internal", "dev"), { recursive: true });
  await copyFile(join(repoRoot, "docker", "internal", "dev", "compose"), join(devRoot, "docker", "internal", "dev", "compose"));
  await chmod(join(devRoot, "docker", "internal", "dev", "compose"), 0o755);
  await copyFile(join(repoRoot, "docker", "internal", "host-profile.sh"), join(devRoot, "docker", "internal", "host-profile.sh"));
  await writeFile(join(devRoot, "docker", "compose.dev.yml"), "name: pi-web-dev\nservices: {}\n", "utf8");
  return devRoot;
}

async function createDevRepoFixtureWithFakeHelper(logPath: string): Promise<string> {
  const devRoot = join(tempDir, "dev-repo-fake-helper");
  const helperPath = join(devRoot, "docker", "internal", "dev", "compose");
  await mkdir(dirname(helperPath), { recursive: true });
  await writeFile(helperPath, `#!/usr/bin/env sh
set -eu
printf 'allow=%s args=%s\n' "\${PI_WEB_DOCKER_ALLOW_ROOT:-}" "$*" >>${shellSingleQuote(logPath)}
`, "utf8");
  await chmod(helperPath, 0o755);
  return devRoot;
}

async function createCleanDevGitRepoWithFakeHelper(logPath: string): Promise<string> {
  const devRoot = await createDevRepoFixtureWithFakeHelper(logPath);
  await Promise.all([
    writeFile(join(devRoot, "staged.txt"), "clean\n", "utf8"),
    writeFile(join(devRoot, "modified.txt"), "clean\n", "utf8"),
  ]);
  const env = cleanProcessEnv();
  await execUtf8("git", ["init", "--quiet", devRoot], env);
  await execUtf8("git", ["-C", devRoot, "add", "."], env);
  await execUtf8("git", [
    "-C", devRoot,
    "-c", "user.name=PI WEB Test",
    "-c", "user.email=pi-web-test@example.invalid",
    "-c", "core.hooksPath=/dev/null",
    "commit", "--quiet", "--no-gpg-sign", "-m", "test fixture",
  ], env);
  return devRoot;
}

async function createDevGeneratedEnv(ids: { uid: number; gid: number; dockerGid: number }): Promise<string> {
  const devRoot = join(tempDir, "dev-runtime");
  await mkdir(join(devRoot, ".pi-web"), { recursive: true });
  await writeFile(join(devRoot, ".pi-web", "docker-compose-dev.generated.env"), [
    `PI_WEB_UID=${String(ids.uid)}`,
    `PI_WEB_GID=${String(ids.gid)}`,
    `DOCKER_GID=${String(ids.dockerGid)}`,
    `PI_WEB_DOCKER_DATA_DIR=${join(tempDir, "dev-data")}`,
    `PI_WEB_DOCKER_DEV_REPO_ROOT=${devRoot}`,
    "PI_WEB_DEV_API_BIND_ADDR=127.0.0.1",
    "PI_WEB_DEV_BIND_ADDR=127.0.0.1",
    "PI_WEB_DEV_API_PORT=8504",
    "PI_WEB_DEV_PORT=8505",
    "PI_WEB_DEV_IMAGE=pi-web:test",
    "COMPOSE_PROJECT_NAME=pi-web-dev-test",
    "",
  ].join("\n"), "utf8");
  return devRoot;
}

async function installFakeDocker(): Promise<FakeDocker> {
  const binDir = join(tempDir, "bin");
  const logPath = join(tempDir, "docker.log");
  const dockerPath = join(binDir, "docker");
  await mkdir(binDir, { recursive: true });
  await writeFile(dockerPath, `#!/usr/bin/env sh
set -eu
: "\${FAKE_DOCKER_LOG:?}"
printf '%s\n' "$*" >>"$FAKE_DOCKER_LOG"
case "\${1:-}" in
  --version)
    printf 'Docker version 99.0.0, fake\n'
    exit 0
    ;;
  context)
    case "\${2:-}" in
      show)
        printf 'default\n'
        exit 0
        ;;
      inspect)
        exit 0
        ;;
    esac
    ;;
  info)
    if [ "\${2:-}" = --format ]; then
      printf 'Docker Desktop\n'
    else
      printf 'Fake Docker info\n'
    fi
    exit 0
    ;;
  compose)
    if [ "\${2:-}" = version ]; then
      exit 0
    fi
    printf 'fake docker'
    for arg in "$@"; do
      printf ' %s' "$arg"
    done
    printf '\n'
    exit 0
    ;;
  container)
    if [ "\${2:-}" = inspect ]; then
      for arg in "$@"; do
        if [ "$arg" = --format ]; then
          printf 'pi-web:test\n'
          exit 0
        fi
      done
      printf '{}\n'
      exit 0
    fi
    ;;
  ps|rm)
    exit 0
    ;;
  run)
    printf 'fake-helper-container-id\n'
    exit 0
    ;;
  logs)
    printf 'fake helper log\n'
    exit 0
    ;;
  inspect)
    for arg in "$@"; do
      case "$arg" in
        *State.ExitCode*)
          printf '0\n'
          exit 0
          ;;
      esac
    done
    printf '{}\n'
    exit 0
    ;;
esac
printf 'unexpected fake docker args: %s\n' "$*" >&2
exit 9
`, "utf8");
  await chmod(dockerPath, 0o755);
  return { binDir, logPath };
}

async function installFakeCurl(binDir: string): Promise<void> {
  const curlPath = join(binDir, "curl");
  await writeFile(curlPath, `#!/usr/bin/env sh
set -eu
asset_root=${shellSingleQuote(join(repoRoot, "docker"))}
output=
url=
while [ "$#" -gt 0 ]; do
  case "$1" in
    -o)
      shift
      output=\${1:-}
      ;;
    -*)
      ;;
    *)
      url=$1
      ;;
  esac
  if [ "$#" -gt 0 ]; then
    shift
  fi
done
[ -n "$url" ] || { printf '%s\n' "fake curl missing URL" >&2; exit 2; }
[ -n "$output" ] || { printf '%s\n' "fake curl missing -o output" >&2; exit 2; }
case "$url" in
  */docker/*) rel=\${url##*/docker/} ;;
  *) printf 'unexpected fake curl url: %s\n' "$url" >&2; exit 2 ;;
esac
src=$asset_root/$rel
[ -f "$src" ] || { printf 'missing fake curl asset: %s\n' "$src" >&2; exit 2; }
mkdir -p "$(dirname "$output")"
cp "$src" "$output"
`, "utf8");
  await chmod(curlPath, 0o755);
}

async function installFakeUname(binDir: string, osName: string): Promise<void> {
  const unamePath = join(binDir, "uname");
  await writeFile(unamePath, `#!/usr/bin/env sh
set -eu
printf '%s\n' ${shellSingleQuote(osName)}
`, "utf8");
  await chmod(unamePath, 0o755);
}

async function installFakeId(binDir: string, uid: number, gid: number): Promise<void> {
  const idPath = join(binDir, "id");
  await writeFile(idPath, `#!/usr/bin/env sh
set -eu
case "\${1:-}" in
  -u) printf '%s\n' ${String(uid)} ;;
  -g) printf '%s\n' ${String(gid)} ;;
  *) printf '%s\n' ${String(uid)} ;;
esac
`, "utf8");
  await chmod(idPath, 0o755);
}

async function withUnixSocket<T>(socketPath: string, callback: () => Promise<T>): Promise<T> {
  await mkdir(dirname(socketPath), { recursive: true });
  const server = createServer();
  await new Promise<void>((resolvePromise, reject) => {
    server.once("error", reject);
    server.listen(socketPath, resolvePromise);
  });
  try {
    return await callback();
  } finally {
    await new Promise<void>((resolvePromise) => {
      server.close(() => {
        resolvePromise();
      });
    });
    await rm(socketPath, { force: true });
  }
}

function cleanProcessEnv(): NodeJS.ProcessEnv {
  const env = sanitizedGitEnv(process.env);
  for (const key of Object.keys(env)) {
    if (key === "COMPOSE_PROJECT_NAME" || key === "DOCKER_GID" || key === "HOSTEXEC_IMAGE" || key === "XDG_DATA_HOME" || key.startsWith("PI_WEB_")) {
      Reflect.deleteProperty(env, key);
    }
  }
  return env;
}

function runtimeEnv(fakeDocker: FakeDocker, installDir: string): NodeJS.ProcessEnv {
  return {
    ...runtimeHostEnv(fakeDocker, installDir),
    PI_WEB_DOCKER_RUNTIME: "1",
  };
}

function runtimeHostEnv(fakeDocker: FakeDocker, installDir: string): NodeJS.ProcessEnv {
  return {
    ...cleanProcessEnv(),
    PATH: `${fakeDocker.binDir}:${process.env["PATH"] ?? ""}`,
    FAKE_DOCKER_LOG: fakeDocker.logPath,
    PI_WEB_DOCKER_RUNTIME: "0",
    PI_WEB_DOCKER_MODE: "runtime",
    PI_WEB_DOCKER_INSTALL_DIR: installDir,
  };
}

function devHostEnv(fakeDocker: FakeDocker, devRoot: string, home: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
  return {
    ...cleanProcessEnv(),
    ...extra,
    PATH: `${fakeDocker.binDir}:${process.env["PATH"] ?? ""}`,
    HOME: home,
    DOCKER_HOST: "",
    FAKE_DOCKER_LOG: fakeDocker.logPath,
    PI_WEB_DOCKER_RUNTIME: "0",
    PI_WEB_DOCKER_MODE: "dev",
    PI_WEB_DOCKER_DEV_REPO_ROOT: devRoot,
  };
}

function devRuntimeEnv(fakeDocker: FakeDocker, devRoot: string): NodeJS.ProcessEnv {
  return {
    ...cleanProcessEnv(),
    PATH: `${fakeDocker.binDir}:${process.env["PATH"] ?? ""}`,
    FAKE_DOCKER_LOG: fakeDocker.logPath,
    PI_WEB_DOCKER_RUNTIME: "1",
    PI_WEB_DOCKER_MODE: "dev",
    PI_WEB_DOCKER_DEV_REPO_ROOT: devRoot,
    PI_WEB_DOCKER_CONTAINER_ID: "current-container",
  };
}

function shellSingleQuote(value: string): string {
  return `'${value.replaceAll("'", "'\\''")}'`;
}
