// Copyright 2018-2026 the Deno authors. MIT license.

import {
  assert,
  assertEquals,
  assertRejects,
  assertStringIncludes,
  assertThrows,
} from "./test_util.ts";

Deno.test(
  { permissions: { write: true, run: true, read: true } },
  async function commandWithCwdIsAsync() {
    const enc = new TextEncoder();
    const cwd = await Deno.makeTempDir({ prefix: "deno_command_test" });

    const exitCodeFileLock = "deno_was_here.lock";
    const exitCodeFile = "deno_was_here";
    const programFile = "poll_exit.ts";
    const program = `
const file = await Deno.open("${exitCodeFileLock}", { write: true, create: true });
async function tryExit() {
  await file.lock(true);
  try {
    const code = parseInt(await Deno.readTextFile("${exitCodeFile}"));
    Deno.exit(code);
  } catch {
    // Retry if we got here before deno wrote the file.
    setTimeout(tryExit, 0.01);
  } finally {
    await file.unlock();
  }
}

tryExit();
`;
    Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));

    const command = new Deno.Command(Deno.execPath(), {
      cwd,
      args: ["run", "-RW", programFile],
      stdout: "inherit",
      stderr: "inherit",
    });
    const child = command.spawn();

    // Write the expected exit code *after* starting deno.
    // This is how we verify that `Child` is actually asynchronous.
    const code = 84;

    await using file = await Deno.open(`${cwd}/${exitCodeFileLock}`, {
      write: true,
      create: true,
    });
    await file.lock(true);
    Deno.writeFileSync(`${cwd}/${exitCodeFile}`, enc.encode(`${code}`));
    await file.unlock();
    const status = await child.status;
    await Deno.remove(cwd, { recursive: true });
    assertEquals(status.code, code);
    assertEquals(status.success, false);
    assertEquals(status.signal, null);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandStdinPiped() {
    const command = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        `
        const buffer = new Uint8Array(5);
        await Deno.stdin.read(buffer);
        if (new TextDecoder().decode(buffer) !== "hello") {
          throw new Error('Expected \\'hello\\'')
        }
        `,
      ],
      stdin: "piped",
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();

    assertThrows(
      () => child.stdout,
      TypeError,
      "Cannot get 'stdout': 'stdout' is not piped",
    );
    assertThrows(
      () => child.stderr,
      TypeError,
      "Cannot get 'stderr': 'stderr' is not piped",
    );

    const msg = new TextEncoder().encode("hello");
    const writer = child.stdin.getWriter();
    await writer.write(msg);
    writer.releaseLock();

    await child.stdin.close();
    const status = await child.status;
    assertEquals(status.success, true);
    assertEquals(status.code, 0);
    assertEquals(status.signal, null);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandStdinPiped() {
    const command = new Deno.Command(Deno.execPath(), {
      args: ["info"],
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();

    assertThrows(
      () => child.stdin,
      TypeError,
      "Cannot get 'stdin': 'stdin' is not piped",
    );
    assertThrows(
      () => child.stdout,
      TypeError,
      "Cannot get 'stdout': 'stdout' is not piped",
    );
    assertThrows(
      () => child.stderr,
      TypeError,
      "Cannot get 'stderr': 'stderr' is not piped",
    );

    await child.status;
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandStdoutPiped() {
    const command = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "await Deno.stdout.write(new TextEncoder().encode('hello'))",
      ],
      stderr: "null",
      stdout: "piped",
    });
    const child = command.spawn();

    assertThrows(
      () => child.stdin,
      TypeError,
      "Cannot get 'stdin': 'stdin' is not piped",
    );
    assertThrows(
      () => child.stderr,
      TypeError,
      "Cannot get 'stderr': 'stderr' is not piped",
    );

    const readable = child.stdout.pipeThrough(new TextDecoderStream());
    const reader = readable.getReader();
    const res = await reader.read();
    assert(!res.done);
    assertEquals(res.value, "hello");

    const resEnd = await reader.read();
    assert(resEnd.done);
    assertEquals(resEnd.value, undefined);
    reader.releaseLock();

    const status = await child.status;
    assertEquals(status.success, true);
    assertEquals(status.code, 0);
    assertEquals(status.signal, null);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandStderrPiped() {
    const command = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "await Deno.stderr.write(new TextEncoder().encode('hello'))",
      ],
      stdout: "null",
      stderr: "piped",
    });
    const child = command.spawn();

    assertThrows(
      () => child.stdin,
      TypeError,
      "Cannot get 'stdin': 'stdin' is not piped",
    );
    assertThrows(
      () => child.stdout,
      TypeError,
      "Cannot get 'stdout': 'stdout' is not piped",
    );

    const readable = child.stderr.pipeThrough(new TextDecoderStream());
    const reader = readable.getReader();
    const res = await reader.read();
    assert(!res.done);
    assertEquals(res.value, "hello");

    const resEnd = await reader.read();
    assert(resEnd.done);
    assertEquals(resEnd.value, undefined);
    reader.releaseLock();

    const status = await child.status;
    assertEquals(status.success, true);
    assertEquals(status.code, 0);
    assertEquals(status.signal, null);
  },
);

Deno.test(
  { permissions: { run: true, write: true, read: true } },
  async function commandRedirectStdoutStderr() {
    const tempDir = await Deno.makeTempDir();
    const fileName = tempDir + "/redirected_stdio.txt";
    const file = await Deno.open(fileName, {
      create: true,
      write: true,
    });

    const command = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));",
      ],
      stdout: "piped",
      stderr: "piped",
    });
    const child = command.spawn();
    await child.stdout.pipeTo(file.writable, {
      preventClose: true,
    });
    await child.stderr.pipeTo(file.writable);
    await child.status;

    const fileContents = await Deno.readFile(fileName);
    const decoder = new TextDecoder();
    const text = decoder.decode(fileContents);

    assertStringIncludes(text, "error");
    assertStringIncludes(text, "output");
  },
);

Deno.test(
  { permissions: { run: true, write: true, read: true } },
  async function commandRedirectStdin() {
    const tempDir = await Deno.makeTempDir();
    const fileName = tempDir + "/redirected_stdio.txt";
    await Deno.writeTextFile(fileName, "hello");
    const file = await Deno.open(fileName);

    const command = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        `
        const buffer = new Uint8Array(5);
        await Deno.stdin.read(buffer);
        if (new TextDecoder().decode(buffer) !== "hello") {
          throw new Error('Expected \\'hello\\'')
        }
        `,
      ],
      stdin: "piped",
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();
    await file.readable.pipeTo(child.stdin, {
      preventClose: true,
    });

    await child.stdin.close();
    const status = await child.status;
    assertEquals(status.code, 0);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandKillSuccess() {
    const command = new Deno.Command(Deno.execPath(), {
      args: ["eval", "setTimeout(() => {}, 10000)"],
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();

    child.kill("SIGKILL");
    const status = await child.status;

    assertEquals(status.success, false);
    if (Deno.build.os === "windows") {
      assertEquals(status.code, 1);
      assertEquals(status.signal, null);
    } else {
      assertEquals(status.code, 137);
      assertEquals(status.signal, "SIGKILL");
    }
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function childProcessExplicitResourceManagement() {
    let dead = undefined;
    {
      const command = new Deno.Command(Deno.execPath(), {
        args: ["eval", "setTimeout(() => {}, 10000)"],
        stdout: "null",
        stderr: "null",
      });
      await using child = command.spawn();
      child.status.then(({ signal }) => {
        dead = signal;
      });
    }

    if (Deno.build.os == "windows") {
      assertEquals(dead, null);
    } else {
      assertEquals(dead, "SIGTERM");
    }
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function childProcessExplicitResourceManagementManualClose() {
    const command = new Deno.Command(Deno.execPath(), {
      args: ["eval", "setTimeout(() => {}, 10000)"],
      stdout: "null",
      stderr: "null",
    });
    await using child = command.spawn();
    child.kill("SIGTERM");
    await child.status;
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandKillFailed() {
    const command = new Deno.Command(Deno.execPath(), {
      args: ["eval", "setTimeout(() => {}, 5000)"],
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();

    assertThrows(() => {
      // @ts-expect-error testing runtime error of bad signal
      child.kill("foobar");
    }, TypeError);

    await child.status;
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandKillOptional() {
    const command = new Deno.Command(Deno.execPath(), {
      args: ["eval", "setTimeout(() => {}, 10000)"],
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();

    child.kill();
    const status = await child.status;

    assertEquals(status.success, false);
    if (Deno.build.os === "windows") {
      assertEquals(status.code, 1);
      assertEquals(status.signal, null);
    } else {
      assertEquals(status.code, 143);
      assertEquals(status.signal, "SIGTERM");
    }
  },
);

Deno.test(
  {
    permissions: { run: true, read: true },
    ignore: Deno.build.os === "windows",
  },
  async function commandKillWithIntegerSignal() {
    const command = new Deno.Command(Deno.execPath(), {
      args: ["eval", "setTimeout(() => {}, 10000)"],
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();

    // Kill with integer signal (9 = SIGKILL)
    child.kill(9);
    const status = await child.status;

    assertEquals(status.success, false);
    assertEquals(status.code, 137);
    assertEquals(status.signal, "SIGKILL");
  },
);

Deno.test(
  {
    permissions: { run: true, read: true },
    ignore: Deno.build.os === "windows",
  },
  async function commandKillWithSignalZero() {
    const command = new Deno.Command(Deno.execPath(), {
      args: ["eval", "setTimeout(() => {}, 10000)"],
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();

    // Signal 0 checks if the process exists
    child.kill(0); // Should not actually kill the process

    // Now kill it for real
    child.kill("SIGTERM");
    const status = await child.status;

    assertEquals(status.success, false);
    assertEquals(status.code, 143);
    assertEquals(status.signal, "SIGTERM");
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandAbort() {
    const ac = new AbortController();
    const command = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "setTimeout(console.log, 1e8)",
      ],
      signal: ac.signal,
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();
    queueMicrotask(() => ac.abort());
    const status = await child.status;
    assertEquals(status.success, false);
    if (Deno.build.os === "windows") {
      assertEquals(status.code, 1);
      assertEquals(status.signal, null);
    } else {
      assertEquals(status.success, false);
      assertEquals(status.code, 143);
    }
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandOutputAbort() {
    const ac = new AbortController();
    const command = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "setTimeout(console.log, 1e8)",
      ],
      signal: ac.signal,
      stdout: "null",
      stderr: "null",
    });
    const start = performance.now();
    queueMicrotask(() => ac.abort());
    const output = await command.output();
    const duration = performance.now() - start;
    assert(
      duration < 5000,
      `Expected output() to be aborted promptly, but ran for ${
        duration.toFixed(2)
      }ms`,
    );
    assertEquals(output.success, false);
    if (Deno.build.os === "windows") {
      assertEquals(output.code, 1);
      assertEquals(output.signal, null);
    } else {
      assertEquals(output.code, 143);
    }
  },
);

Deno.test(
  { permissions: { read: true, run: false } },
  async function commandPermissions() {
    await assertRejects(async () => {
      await new Deno.Command(Deno.execPath(), {
        args: ["eval", "console.log('hello world')"],
      }).output();
    }, Deno.errors.NotCapable);
  },
);

Deno.test(
  { permissions: { read: true, run: false } },
  function commandSyncPermissions() {
    assertThrows(() => {
      new Deno.Command(Deno.execPath(), {
        args: ["eval", "console.log('hello world')"],
      }).outputSync();
    }, Deno.errors.NotCapable);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandSuccess() {
    const output = await new Deno.Command(Deno.execPath(), {
      args: ["eval", "console.log('hello world')"],
    }).output();

    assertEquals(output.success, true);
    assertEquals(output.code, 0);
    assertEquals(output.signal, null);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  function commandSyncSuccess() {
    const output = new Deno.Command(Deno.execPath(), {
      args: ["eval", "console.log('hello world')"],
    }).outputSync();

    assertEquals(output.success, true);
    assertEquals(output.code, 0);
    assertEquals(output.signal, null);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandUrl() {
    const output = await new Deno.Command(
      new URL(`file:///${Deno.execPath()}`),
      {
        args: ["eval", "console.log('hello world')"],
      },
    ).output();

    assertEquals(new TextDecoder().decode(output.stdout), "hello world\n");

    assertEquals(output.success, true);
    assertEquals(output.code, 0);
    assertEquals(output.signal, null);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  function commandSyncUrl() {
    const output = new Deno.Command(
      new URL(`file:///${Deno.execPath()}`),
      {
        args: ["eval", "console.log('hello world')"],
      },
    ).outputSync();

    assertEquals(new TextDecoder().decode(output.stdout), "hello world\n");

    assertEquals(output.success, true);
    assertEquals(output.code, 0);
    assertEquals(output.signal, null);
  },
);

Deno.test({ permissions: { run: true } }, function commandNotFound() {
  assertThrows(
    () => new Deno.Command("this file hopefully doesn't exist").output(),
    Deno.errors.NotFound,
  );
});

Deno.test({ permissions: { run: true } }, function commandSyncNotFound() {
  assertThrows(
    () => new Deno.Command("this file hopefully doesn't exist").outputSync(),
    Deno.errors.NotFound,
  );
});

Deno.test({ permissions: { run: true, read: true } }, function cwdNotFound() {
  assertThrows(
    () =>
      new Deno.Command(Deno.execPath(), {
        cwd: Deno.cwd() + "/non-existent-directory",
      }).output(),
    Deno.errors.NotFound,
    "No such cwd",
  );
});

Deno.test(
  { permissions: { run: true, read: true } },
  function cwdNotDirectory() {
    assertThrows(
      () =>
        new Deno.Command(Deno.execPath(), {
          cwd: Deno.execPath(),
        }).output(),
      Deno.errors.NotFound,
      "cwd is not a directory",
    );
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandFailedWithCode() {
    const output = await new Deno.Command(Deno.execPath(), {
      args: ["eval", "Deno.exit(41 + 1)"],
    }).output();
    assertEquals(output.success, false);
    assertEquals(output.code, 42);
    assertEquals(output.signal, null);
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  function commandSyncFailedWithCode() {
    const output = new Deno.Command(Deno.execPath(), {
      args: ["eval", "Deno.exit(41 + 1)"],
    }).outputSync();
    assertEquals(output.success, false);
    assertEquals(output.code, 42);
    assertEquals(output.signal, null);
  },
);

Deno.test(
  {
    permissions: { run: true, read: true },
  },
  async function commandFailedWithSignal() {
    const output = await new Deno.Command(Deno.execPath(), {
      args: ["eval", "Deno.kill(Deno.pid, 'SIGKILL')"],
    }).output();
    assertEquals(output.success, false);
    if (Deno.build.os === "windows") {
      assertEquals(output.code, 1);
      assertEquals(output.signal, null);
    } else {
      assertEquals(output.code, 128 + 9);
      assertEquals(output.signal, "SIGKILL");
    }
  },
);

Deno.test(
  {
    permissions: { run: true, read: true },
  },
  function commandSyncFailedWithSignal() {
    const output = new Deno.Command(Deno.execPath(), {
      args: ["eval", "Deno.kill(Deno.pid, 'SIGKILL')"],
    }).outputSync();
    assertEquals(output.success, false);
    if (Deno.build.os === "windows") {
      assertEquals(output.code, 1);
      assertEquals(output.signal, null);
    } else {
      assertEquals(output.code, 128 + 9);
      assertEquals(output.signal, "SIGKILL");
    }
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandOutput() {
    const { stdout } = await new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "await Deno.stdout.write(new TextEncoder().encode('hello'))",
      ],
    }).output();

    const s = new TextDecoder().decode(stdout);
    assertEquals(s, "hello");
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  function commandSyncOutput() {
    const { stdout } = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "await Deno.stdout.write(new TextEncoder().encode('hello'))",
      ],
    }).outputSync();

    const s = new TextDecoder().decode(stdout);
    assertEquals(s, "hello");
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandStderrOutput() {
    const { stderr } = await new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "await Deno.stderr.write(new TextEncoder().encode('error'))",
      ],
    }).output();

    const s = new TextDecoder().decode(stderr);
    assertEquals(s, "error");
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  function commandSyncStderrOutput() {
    const { stderr } = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "await Deno.stderr.write(new TextEncoder().encode('error'))",
      ],
    }).outputSync();

    const s = new TextDecoder().decode(stderr);
    assertEquals(s, "error");
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandEnv() {
    const { stdout } = await new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
      ],
      env: {
        FOO: "0123",
        BAR: "4567",
      },
    }).output();
    const s = new TextDecoder().decode(stdout);
    assertEquals(s, "01234567");
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  function commandSyncEnv() {
    const { stdout } = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
      ],
      env: {
        FOO: "0123",
        BAR: "4567",
      },
    }).outputSync();
    const s = new TextDecoder().decode(stdout);
    assertEquals(s, "01234567");
  },
);

Deno.test(
  { permissions: { run: true, read: true, env: true } },
  async function commandClearEnv() {
    Deno.env.set("DENO_COMMAND_CLEAR_ENV_TESTING", "TESTING");
    const { stdout } = await new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "-p",
        "JSON.stringify(Deno.env.toObject())",
      ],
      clearEnv: true,
      env: {
        FOO: "23147",
      },
    }).output();

    const obj = JSON.parse(new TextDecoder().decode(stdout));

    // can't check for object equality because the OS may set additional env
    // vars for processes, so we check if PATH isn't present as that is a common
    // env var across OS's and isn't set for processes.
    assertEquals(obj.FOO, "23147");
    assert(!("DENO_COMMAND_CLEAR_ENV_TESTING" in obj));
    Deno.env.delete("DENO_COMMAND_CLEAR_ENV_TESTING");
  },
);

Deno.test(
  { permissions: { run: true, read: true, env: true } },
  function commandSyncClearEnv() {
    Deno.env.set("DENO_COMMAND_SYNC_CLEAR_ENV_TESTING", "TESTING");
    const { stdout } = new Deno.Command(Deno.execPath(), {
      args: [
        "eval",
        "-p",
        "JSON.stringify(Deno.env.toObject())",
      ],
      clearEnv: true,
      env: {
        FOO: "23147",
      },
    }).outputSync();

    const obj = JSON.parse(new TextDecoder().decode(stdout));

    // can't check for object equality because the OS may set additional env
    // vars for processes, so we check if PATH isn't present as that is a common
    // env var across OS's and isn't set for processes.
    assertEquals(obj.FOO, "23147");
    assert(!("DENO_COMMAND_SYNC_CLEAR_ENV_TESTING" in obj));
    Deno.env.delete("DENO_COMMAND_SYNC_CLEAR_ENV_TESTING");
  },
);

Deno.test(
  {
    permissions: { run: true, read: true },
    ignore: Deno.build.os === "windows",
  },
  async function commandUid() {
    const { stdout } = await new Deno.Command("id", {
      args: ["-u"],
    }).output();

    const currentUid = new TextDecoder().decode(stdout);

    if (currentUid !== "0") {
      await assertRejects(async () => {
        await new Deno.Command("echo", {
          args: ["fhqwhgads"],
          uid: 0,
        }).output();
      }, Deno.errors.PermissionDenied);
    }
  },
);

Deno.test(
  {
    permissions: { run: true, read: true, write: true },
    ignore: Deno.build.os !== "windows",
  },
  async function rejectBatAndCmdFiles() {
    const tempDir = await Deno.makeTempDir();
    Deno.writeTextFileSync(tempDir + "/test.bat", "@echo off\r\necho 1 2 3 %*");
    for (
      const ext of [
        ".bat",
        ".BaT",
        ".bAT",
        ".BAT",
        ".bat.",
        ".bat  ",
        ".bat . ",
      ]
    ) {
      const fileName = tempDir + "/test" + ext;
      const output = await new Deno.Command(fileName, {
        args: ["&calc.exe"],
        stdout: "piped",
      }).output();
      const stdout = new TextDecoder().decode(output.stdout);
      // should have calc escaped here instead of executing it
      assert(stdout.includes(`1 2 3 "&calc.exe\"`), `Text: ${stdout}`);
    }
  },
);

Deno.test(
  {
    permissions: { run: true, read: true },
    ignore: Deno.build.os === "windows",
  },
  function commandSyncUid() {
    const { stdout } = new Deno.Command("id", {
      args: ["-u"],
    }).outputSync();

    const currentUid = new TextDecoder().decode(stdout);

    if (currentUid !== "0") {
      assertThrows(() => {
        new Deno.Command("echo", {
          args: ["fhqwhgads"],
          uid: 0,
        }).outputSync();
      }, Deno.errors.PermissionDenied);
    }
  },
);

Deno.test(
  {
    permissions: { run: true, read: true },
    ignore: Deno.build.os === "windows",
  },
  async function commandGid() {
    const { stdout } = await new Deno.Command("id", {
      args: ["-g"],
    }).output();

    const currentGid = new TextDecoder().decode(stdout);

    if (currentGid !== "0") {
      await assertRejects(async () => {
        await new Deno.Command("echo", {
          args: ["fhqwhgads"],
          gid: 0,
        }).output();
      }, Deno.errors.PermissionDenied);
    }
  },
);

Deno.test(
  {
    permissions: { run: true, read: true },
    ignore: Deno.build.os === "windows",
  },
  function commandSyncGid() {
    const { stdout } = new Deno.Command("id", {
      args: ["-g"],
    }).outputSync();

    const currentGid = new TextDecoder().decode(stdout);

    if (currentGid !== "0") {
      assertThrows(() => {
        new Deno.Command("echo", {
          args: ["fhqwhgads"],
          gid: 0,
        }).outputSync();
      }, Deno.errors.PermissionDenied);
    }
  },
);

Deno.test(function commandStdinPipedFails() {
  assertThrows(
    () =>
      new Deno.Command("id", {
        stdin: "piped",
      }).output(),
    TypeError,
    "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
  );
});

Deno.test(function spawnSyncStdinPipedFails() {
  assertThrows(
    () =>
      new Deno.Command("id", {
        stdin: "piped",
      }).outputSync(),
    TypeError,
    "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
  );
});

Deno.test(
  // FIXME(bartlomieju): this test is very flaky on CI, fix it
  {
    ignore: true,
    permissions: { write: true, run: true, read: true },
  },
  async function commandChildUnref() {
    const enc = new TextEncoder();
    const cwd = await Deno.makeTempDir({ prefix: "deno_command_test" });

    const programFile = "unref.ts";
    const program = `
const command = await new Deno.Command(Deno.execPath(), {
  cwd: Deno.args[0],
  stdout: "piped",
  args: ["run", "-A", Deno.args[1]],
});
const child = command.spawn();
const readable = child.stdout.pipeThrough(new TextDecoderStream());
const reader = readable.getReader();
// set up an interval that will end after reading a few messages from stdout,
// to verify that stdio streams are properly unrefed
let count = 0;
let interval;
interval = setInterval(async () => {
  count += 1;
  if (count > 10) {
    clearInterval(interval);
    console.log("cleared interval");
  }
  const res = await reader.read();
  if (res.done) {
    throw new Error("stream shouldn't be done");
  }
  if (res.value.trim() != "hello from interval") {
    throw new Error("invalid message received");
  }
}, 120);
console.log("spawned pid", child.pid);
child.unref();
`;

    const childProgramFile = "unref_child.ts";
    const childProgram = `
setInterval(() => {
  console.log("hello from interval");
}, 100);
`;
    Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));
    Deno.writeFileSync(`${cwd}/${childProgramFile}`, enc.encode(childProgram));
    // In this subprocess we are spawning another subprocess which has
    // an infinite interval set. Following call would never resolve unless
    // child process gets unrefed.
    const { success, stdout, stderr } = await new Deno.Command(
      Deno.execPath(),
      {
        cwd,
        args: ["run", "-A", programFile, cwd, childProgramFile],
      },
    ).output();

    assert(success);
    const stdoutText = new TextDecoder().decode(stdout);
    const stderrText = new TextDecoder().decode(stderr);
    assert(stderrText.length == 0);
    const [line1, line2] = stdoutText.split("\n");
    const pidStr = line1.split(" ").at(-1);
    assert(pidStr);
    assertEquals(line2, "cleared interval");
    const pid = Number.parseInt(pidStr, 10);
    await Deno.remove(cwd, { recursive: true });
    // Child process should have been killed when parent process exits.
    assertThrows(() => {
      Deno.kill(pid, "SIGTERM");
    }, Deno.errors.NotFound);
  },
);

Deno.test(
  { ignore: Deno.build.os !== "windows" },
  async function commandWindowsRawArguments() {
    let { success, stdout } = await new Deno.Command("cmd", {
      args: ["/d", "/s", "/c", '"deno ^"--version^""'],
      windowsRawArguments: true,
    }).output();
    assert(success);
    let stdoutText = new TextDecoder().decode(stdout);
    assertStringIncludes(stdoutText, "deno");
    assertStringIncludes(stdoutText, "v8");
    assertStringIncludes(stdoutText, "typescript");

    ({ success, stdout } = new Deno.Command("cmd", {
      args: ["/d", "/s", "/c", '"deno ^"--version^""'],
      windowsRawArguments: true,
    }).outputSync());
    assert(success);
    stdoutText = new TextDecoder().decode(stdout);
    assertStringIncludes(stdoutText, "deno");
    assertStringIncludes(stdoutText, "v8");
    assertStringIncludes(stdoutText, "typescript");
  },
);

Deno.test(
  { permissions: { read: true, run: true } },
  async function commandWithPrototypePollution() {
    const originalThen = Promise.prototype.then;
    const originalSymbolIterator = Array.prototype[Symbol.iterator];
    try {
      Promise.prototype.then = Array.prototype[Symbol.iterator] = () => {
        throw new Error();
      };
      await new Deno.Command(Deno.execPath(), {
        args: ["eval", "console.log('hello world')"],
      }).output();
    } finally {
      Promise.prototype.then = originalThen;
      Array.prototype[Symbol.iterator] = originalSymbolIterator;
    }
  },
);

Deno.test(
  { permissions: { run: true, read: true } },
  async function commandKillAfterStatus() {
    const command = new Deno.Command(Deno.execPath(), {
      args: ["help"],
      stdout: "null",
      stderr: "null",
    });
    const child = command.spawn();
    await child.status;
    assertThrows(
      () => child.kill(),
      TypeError,
      "Child process has already terminated",
    );
  },
);

Deno.test(
  "process that fails to spawn, prints its name in error",
  async () => {
    assertThrows(
      () => new Deno.Command("doesntexist").outputSync(),
      Error,
      "Failed to spawn 'doesntexist'",
    );
    await assertRejects(
      async () => await new Deno.Command("doesntexist").output(),
      Error,
      "Failed to spawn 'doesntexist'",
    );
  },
);

Deno.test(
  { permissions: { write: true, run: true, read: true } },
  async function commandWithCwdOrPath() {
    const cwd = Deno.makeTempDirSync({ prefix: "deno_command_test" });
    try {
      const suffix = Deno.build.os === "windows" ? ".exe" : "";
      Deno.mkdirSync(`${cwd}/subdir`);
      Deno.copyFileSync(Deno.execPath(), `${cwd}/subdir/my_binary${suffix}`);
      // cwd
      {
        const output = await new Deno.Command(`./my_binary${suffix}`, {
          cwd: `${cwd}/subdir`,
          args: ["-v"],
        }).output();
        assertEquals(output.success, true);
      }
      // path
      {
        const output = await new Deno.Command(`my_binary${suffix}`, {
          env: {
            PATH: `${cwd}/subdir`,
          },
          args: ["-v"],
        }).output();
        assertEquals(output.success, true);
      }
    } finally {
      Deno.removeSync(cwd, { recursive: true });
    }
  },
);

Deno.test(async function outputWhenManuallyConsumingStreams() {
  const command = new Deno.Command(Deno.execPath(), {
    args: ["eval", "console.log('hello world')"],
    stdout: "piped",
    stderr: "piped",
  });
  const child = command.spawn();
  for await (const _ of child.stdout) {
    // consume stdout
  }
  for await (const _ of child.stderr) {
    // consume stderr
  }
  const status = await child.output();
  assertEquals(status.success, true);
  assertEquals(status.code, 0);
  assertEquals(status.signal, null);
  assertEquals(status.stdout, new Uint8Array());
  assertEquals(status.stderr, new Uint8Array());
});

Deno.test(
  {
    ignore: Deno.build.os !== "windows",
    permissions: { run: ["cmd"], read: true, env: true },
  },
  async function envCaseInsensitiveWindows() {
    const command = new Deno.Command("cmd", {
      args: ["/d", "/s", "/c", "echo", "1"],
      env: {
        // notice Path is not PATH
        Path: Deno.env.get("PATH")!,
      },
      clearEnv: true,
    });
    const child = await command.output();
    assertEquals(child.success, true);
  },
);

Deno.test(
  { ignore: Deno.build.os === "windows" },
  async function abortChildProcessRightWhenItExitsShouldNotThrow() {
    const controller = new AbortController();
    const cb = () => controller.abort();
    Deno.addSignalListener("SIGCHLD", cb);
    const output = await new Deno.Command("true", { signal: controller.signal })
      .output();
    assertEquals(output.success, true);
    assertEquals(output.code, 0);
    assertEquals(output.signal, null);
    assertEquals(output.stdout, new Uint8Array());
    assertEquals(output.stderr, new Uint8Array());

    Deno.removeSignalListener("SIGCHLD", cb);
  },
);

Deno.test({ permissions: { run: true } }, async function collectArrayBuffer() {
  const process = new Deno.Command(Deno.execPath(), {
    args: ["eval", "console.log('hello')"],
    stdout: "piped",
  }).spawn();

  const output = await process.stdout.arrayBuffer();
  assert(output instanceof ArrayBuffer);
  assertEquals(
    new Uint8Array(output),
    new Uint8Array([104, 101, 108, 108, 111, 10]),
  );

  await process.status;
});

Deno.test({ permissions: { run: true } }, async function collectBytes() {
  const process = new Deno.Command(Deno.execPath(), {
    args: ["eval", "console.log('hello')"],
    stdout: "piped",
  }).spawn();

  const output = await process.stdout.bytes();
  assert(output instanceof Uint8Array);
  assertEquals(output, new Uint8Array([104, 101, 108, 108, 111, 10]));

  await process.status;
});

Deno.test({ permissions: { run: true } }, async function collectJSON() {
  const process = new Deno.Command(Deno.execPath(), {
    args: ["eval", "console.log(JSON.stringify({foo: 'bar'}))"],
    stdout: "piped",
  }).spawn();

  const output = await process.stdout.json();
  assertEquals(output, { foo: "bar" });

  await process.status;
});

Deno.test({ permissions: { run: true } }, async function collectText() {
  const process = new Deno.Command(Deno.execPath(), {
    args: ["eval", "console.log('hello')"],
    stdout: "piped",
  }).spawn();

  const output = await process.stdout.text();
  assertEquals(output, "hello\n");

  await process.status;
});

Deno.test(
  { permissions: { run: true } },
  async function denoSpawnBasic() {
    const child = Deno.spawn(Deno.execPath(), {
      args: ["eval", "console.log('hello from spawn')"],
      stdout: "piped",
      stderr: "null",
    });

    assert(child instanceof Deno.ChildProcess);
    assert(child.pid > 0);

    const output = await child.stdout.text();
    assertEquals(output, "hello from spawn\n");

    const status = await child.status;
    assertEquals(status.success, true);
    assertEquals(status.code, 0);
  },
);

Deno.test(
  { permissions: { run: true } },
  async function denoSpawnDefaultsToInherit() {
    // Deno.spawn should default stdio to "inherit" like Command.spawn()
    const child = Deno.spawn(Deno.execPath(), {
      args: ["eval", "Deno.exit(42)"],
    });

    const status = await child.status;
    assertEquals(status.success, false);
    assertEquals(status.code, 42);
  },
);

Deno.test(
  { permissions: { run: true } },
  async function denoSpawnWithPipedStdin() {
    const child = Deno.spawn(Deno.execPath(), {
      args: [
        "eval",
        "const buf = new Uint8Array(5); await Deno.stdin.read(buf); console.log(new TextDecoder().decode(buf))",
      ],
      stdin: "piped",
      stdout: "piped",
      stderr: "null",
    });

    const writer = child.stdin.getWriter();
    await writer.write(new TextEncoder().encode("hello"));
    await writer.close();

    const output = await child.stdout.text();
    assertEquals(output, "hello\n");

    const status = await child.status;
    assertEquals(status.success, true);
  },
);

Deno.test(
  { permissions: { run: true } },
  async function denoSpawnWithArgsArray() {
    const child = Deno.spawn(
      Deno.execPath(),
      ["eval", "console.log('args overload')"],
      { stdout: "piped", stderr: "null" },
    );

    const output = await child.stdout.text();
    assertEquals(output, "args overload\n");

    const status = await child.status;
    assertEquals(status.success, true);
    assertEquals(status.code, 0);
  },
);

Deno.test(
  { permissions: { run: true } },
  function denoSpawnWithArgsArrayAndArgsOptionThrows() {
    assertThrows(
      () => {
        Deno.spawn(
          Deno.execPath(),
          ["eval", "console.log('hello')"],
          // @ts-expect-error - args not allowed when using args array overload
          { args: ["eval", "console.log('hello')"], stdout: "null" },
        );
      },
      TypeError,
      "Passing 'args' in options is not allowed when args are passed as a separate argument",
    );
  },
);

Deno.test(
  { permissions: { run: true } },
  async function denoSpawnAndWaitBasic() {
    const { code, stdout, stderr } = await Deno.spawnAndWait(
      Deno.execPath(),
      {
        args: ["eval", "console.log('waited')"],
      },
    );

    assertEquals(code, 0);
    assertEquals(new TextDecoder().decode(stdout), "waited\n");
    assertEquals(new TextDecoder().decode(stderr), "");
  },
);

Deno.test(
  { permissions: { run: true } },
  async function denoSpawnAndWaitWithArgsArray() {
    const { code, stdout } = await Deno.spawnAndWait(
      Deno.execPath(),
      ["eval", "console.log('args overload waited')"],
    );

    assertEquals(code, 0);
    assertEquals(new TextDecoder().decode(stdout), "args overload waited\n");
  },
);

Deno.test(
  { permissions: { run: true } },
  async function denoSpawnAndWaitExitCode() {
    const { code, success } = await Deno.spawnAndWait(
      Deno.execPath(),
      ["eval", "Deno.exit(42)"],
    );

    assertEquals(success, false);
    assertEquals(code, 42);
  },
);

Deno.test(
  { permissions: { run: true } },
  function denoSpawnAndWaitPipedStdinThrows() {
    assertThrows(
      () => {
        Deno.spawnAndWait(Deno.execPath(), {
          args: ["eval", "console.log('hello')"],
          stdin: "piped",
        });
      },
      TypeError,
      "Piped stdin is not supported for this function",
    );
  },
);

Deno.test(
  { permissions: { run: true } },
  function denoSpawnAndWaitWithArgsArrayAndArgsOptionThrows() {
    assertThrows(
      () => {
        Deno.spawnAndWait(
          Deno.execPath(),
          ["eval", "console.log('hello')"],
          // @ts-expect-error - args not allowed when using args array overload
          { args: ["eval", "console.log('hello')"] },
        );
      },
      TypeError,
      "Passing 'args' in options is not allowed when args are passed as a separate argument",
    );
  },
);

Deno.test(
  { permissions: { run: true } },
  function denoSpawnAndWaitSyncBasic() {
    const { code, stdout, stderr } = Deno.spawnAndWaitSync(
      Deno.execPath(),
      {
        args: ["eval", "console.log('sync waited')"],
      },
    );

    assertEquals(code, 0);
    assertEquals(new TextDecoder().decode(stdout), "sync waited\n");
    assertEquals(new TextDecoder().decode(stderr), "");
  },
);

Deno.test(
  { permissions: { run: true } },
  function denoSpawnAndWaitSyncWithArgsArray() {
    const { code, stdout } = Deno.spawnAndWaitSync(
      Deno.execPath(),
      ["eval", "console.log('sync args overload')"],
    );

    assertEquals(code, 0);
    assertEquals(
      new TextDecoder().decode(stdout),
      "sync args overload\n",
    );
  },
);

Deno.test(
  { permissions: { run: true } },
  function denoSpawnAndWaitSyncExitCode() {
    const { code, success } = Deno.spawnAndWaitSync(
      Deno.execPath(),
      ["eval", "Deno.exit(42)"],
    );

    assertEquals(success, false);
    assertEquals(code, 42);
  },
);

Deno.test(
  { permissions: { run: true } },
  function denoSpawnAndWaitSyncPipedStdinThrows() {
    assertThrows(
      () => {
        Deno.spawnAndWaitSync(Deno.execPath(), {
          args: ["eval", "console.log('hello')"],
          stdin: "piped",
        });
      },
      TypeError,
      "Piped stdin is not supported for this function",
    );
  },
);

Deno.test(
  { permissions: { run: true } },
  function denoSpawnAndWaitSyncWithArgsArrayAndArgsOptionThrows() {
    assertThrows(
      () => {
        Deno.spawnAndWaitSync(
          Deno.execPath(),
          ["eval", "console.log('hello')"],
          // @ts-expect-error - args not allowed when using args array overload
          { args: ["eval", "console.log('hello')"] },
        );
      },
      TypeError,
      "Passing 'args' in options is not allowed when args are passed as a separate argument",
    );
  },
);
