import { describe, expect, test } from "vitest";
import { safeExec, safeSpawn } from "../spawn";

describe("safeExec", () => {
	test("falls back to the process error message when stderr is empty", async () => {
		const result = await safeExec({
			command: process.execPath,
			args: ["-e", "process.stdout.write('a'.repeat(2 * 1024 * 1024))"],
		});

		expect(result.exitCode).toBe(1);
		expect(result.stderr).toContain("stdout maxBuffer length exceeded");
	});

	describe("successful commands", () => {
		test("returns exitCode 0 and output for successful command", async () => {
			const result = await safeExec({ command: "echo", args: ["hello"] });

			expect(result.exitCode).toBe(0);
			expect(result.stdout.trim()).toBe("hello");
			expect(result.stderr).toBe("");
			expect(result.timedOut).toBe(false);
		});
	});

	describe("failed commands", () => {
		test("returns non-zero exitCode for failed command", async () => {
			const result = await safeExec({
				command: "sh",
				args: ["-c", "exit 1"],
			});

			expect(result.exitCode).toBe(1);
			expect(result.timedOut).toBe(false);
		});

		test("captures stderr from failed command", async () => {
			const result = await safeExec({
				command: "sh",
				args: ["-c", "echo 'error message' >&2 && exit 1"],
			});

			expect(result.exitCode).toBe(1);
			expect(result.stderr).toContain("error message");
			expect(result.timedOut).toBe(false);
		});
	});

	describe("timeout handling", () => {
		test("detects timeout and sets timedOut flag", async () => {
			const result = await safeExec({
				command: "sleep",
				args: ["10"],
				timeout: 20,
			});

			expect(result.timedOut).toBe(true);
			expect(result.exitCode).toBe(1);
			expect(result.stderr).toBe("Command timed out before completing");
		});

		test("returns timedOut false when command completes within timeout", async () => {
			const result = await safeExec({
				command: "echo",
				args: ["quick"],
				timeout: 5000,
			});

			expect(result.timedOut).toBe(false);
			expect(result.exitCode).toBe(0);
		});
	});

	describe("env", () => {
		test("passes custom env variables to the command", async () => {
			const result = await safeExec({
				command: "sh",
				args: ["-c", "echo $TEST_EXEC_VAR"],
				env: { TEST_EXEC_VAR: "exec_value" },
			});

			expect(result.exitCode).toBe(0);
			expect(result.stdout.trim()).toBe("exec_value");
		});
	});

	describe("shell injection protection", () => {
		test.each([
			{
				name: "treats semicolon-separated commands as a single literal argument",
				argument: "safe; echo injected",
				expected: "safe; echo injected",
			},
			{
				name: "does not evaluate command substitution syntax",
				argument: "$(echo injected)",
				expected: "$(echo injected)",
			},
			{
				name: "does not expand glob patterns",
				argument: "*.ts",
				expected: "*.ts",
			},
		])("$name", async ({ argument, expected }) => {
			const result = await safeExec({
				command: "echo",
				args: [argument],
			});

			expect(result.exitCode).toBe(0);
			expect(result.stdout.trim()).toBe(expected);
		});
	});

	describe("stdout on failure", () => {
		test("captures stdout written before a non-zero exit", async () => {
			const result = await safeExec({
				command: "sh",
				args: ["-c", "echo output_before_failure && exit 1"],
			});

			expect(result.exitCode).toBe(1);
			expect(result.stdout).toContain("output_before_failure");
		});
	});
});

describe("safeSpawn", () => {
	describe("successful commands", () => {
		test("returns exitCode 0 and correct summary", async () => {
			const result = await safeSpawn({ command: "echo", args: ["hello"] });

			expect(result.exitCode).toBe(0);
			expect(result.summary).toBe("hello");
			expect(result.error).toBe("");
		});

		test("summary is the last non-empty stdout line", async () => {
			const result = await safeSpawn({
				command: "sh",
				args: ["-c", "echo first && echo second && echo third"],
			});

			expect(result.exitCode).toBe(0);
			expect(result.summary).toBe("third");
		});

		test("skips blank and whitespace-only lines when tracking summary", async () => {
			const result = await safeSpawn({
				command: "sh",
				args: ["-c", "printf 'first\\n\\n   \\n'"],
			});

			expect(result.exitCode).toBe(0);
			expect(result.summary).toBe("first");
		});
	});

	describe("callbacks", () => {
		test("calls onStdout once per stdout line", async () => {
			const lines: string[] = [];

			await safeSpawn({
				command: "sh",
				args: ["-c", "echo line1 && echo line2 && echo line3"],
				onStdout: (line) => lines.push(line),
			});

			expect(lines).toEqual(["line1", "line2", "line3"]);
		});

		test("calls onStderr once per stderr line", async () => {
			const errors: string[] = [];

			await safeSpawn({
				command: "sh",
				args: ["-c", "echo err1 >&2 && echo err2 >&2"],
				onStderr: (line) => errors.push(line),
			});

			expect(errors).toEqual(["err1", "err2"]);
		});

		test("calls onSpawn immediately with the child process", async () => {
			let receivedChild: ReturnType<typeof import("node:child_process").spawn> | null = null;

			await safeSpawn({
				command: "echo",
				args: ["test"],
				onSpawn: (child) => {
					receivedChild = child;
				},
			});

			expect(receivedChild).not.toBeNull();
		});
	});

	describe("failed commands", () => {
		test("returns the exact non-zero exit code", async () => {
			const result = await safeSpawn({
				command: "sh",
				args: ["-c", "exit 42"],
			});

			expect(result.exitCode).toBe(42);
		});

		test("error contains the last stderr line", async () => {
			const result = await safeSpawn({
				command: "sh",
				args: ["-c", "echo err_first >&2 && echo err_last >&2 && exit 1"],
			});

			expect(result.exitCode).toBe(1);
			expect(result.error).toBe("err_last");
		});

		test("stderr keeps the captured stderr output", async () => {
			const result = await safeSpawn({
				command: "sh",
				args: ["-c", "echo err_first >&2 && echo err_last >&2 && exit 1"],
			});

			expect(result.stderr).toBe("err_first\nerr_last");
		});

		test("stderr keeps only the last 50 stderr lines", async () => {
			const result = await safeSpawn({
				command: "sh",
				args: ["-c", "i=1; while [ $i -le 55 ]; do echo err_$i >&2; i=$((i+1)); done; exit 1"],
			});

			expect(result.stderr).toBe(Array.from({ length: 50 }, (_, index) => `err_${index + 6}`).join("\n"));
		});

		test("returns exitCode -1 when the command is not found", async () => {
			const result = await safeSpawn({
				command: "this-command-does-not-exist-zerobyte",
				args: [],
			});

			expect(result.exitCode).toBe(-1);
			expect(result.error.length).toBeGreaterThan(0);
		});
	});

	describe("stdoutMode", () => {
		test("raw mode skips readline and leaves summary empty", async () => {
			const result = await safeSpawn({
				command: "echo",
				args: ["hello"],
				stdoutMode: "raw",
				onSpawn: (child) => {
					child.stdout?.resume();
				},
			});

			expect(result.summary).toBe("");
		});

		test("raw mode exposes the raw stdout stream via onSpawn", async () => {
			const chunks: Buffer[] = [];

			await safeSpawn({
				command: "echo",
				args: ["raw_output"],
				stdoutMode: "raw",
				onSpawn: (child) => {
					child.stdout?.on("data", (chunk: Buffer) => chunks.push(chunk));
				},
			});

			const output = Buffer.concat(chunks).toString("utf8").trim();
			expect(output).toBe("raw_output");
		});
	});

	describe("env", () => {
		test("passes custom env variables to the spawned process", async () => {
			const lines: string[] = [];

			await safeSpawn({
				command: "sh",
				args: ["-c", "echo $TEST_SPAWN_VAR"],
				env: { TEST_SPAWN_VAR: "spawn_value" },
				onStdout: (line) => lines.push(line),
			});

			expect(lines).toContain("spawn_value");
		});
	});

	describe("shell injection protection", () => {
		test.each([
			{
				name: "treats semicolon-separated commands as a single literal argument",
				argument: "safe; echo injected",
				expected: "safe; echo injected",
			},
			{
				name: "does not evaluate command substitution syntax",
				argument: "$(echo injected)",
				expected: "$(echo injected)",
			},
			{
				name: "does not expand glob patterns",
				argument: "*.ts",
				expected: "*.ts",
			},
		])("$name", async ({ argument, expected }) => {
			const lines: string[] = [];

			await safeSpawn({
				command: "echo",
				args: [argument],
				onStdout: (line) => lines.push(line),
			});

			expect(lines).toEqual([expected]);
		});
	});
});
