/**
 * cli-key-command.test.ts — unit tests for `sf key` subcommand.
 *
 * Uses an InMemoryAuthStorageBackend so no real auth.json is touched.
 * Tests call runKeyCommand() with a temp auth path and spy on
 * process.stdout/stderr to assert output without spawning a subprocess.
 */

import assert from "node:assert/strict";
import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, test } from "vitest";

import { maskKeyLast4, runKeyCommand } from "../cli-key.js";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeTempAuthPath(): string {
	const dir = join(
		tmpdir(),
		`sf-key-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
	);
	mkdirSync(dir, { recursive: true });
	const authPath = join(dir, "auth.json");
	writeFileSync(authPath, "{}", "utf-8");
	return authPath;
}

/** Capture stdout lines written during an async callback. */
async function captureStdout(fn: () => Promise<unknown>): Promise<string> {
	const chunks: string[] = [];
	const orig = process.stdout.write.bind(process.stdout);
	(process.stdout as NodeJS.WriteStream).write = (chunk: unknown) => {
		chunks.push(String(chunk));
		return true;
	};
	try {
		await fn();
	} finally {
		(process.stdout as NodeJS.WriteStream).write = orig;
	}
	return chunks.join("");
}

/** Capture stderr lines written during an async callback. */
async function captureStderr(fn: () => Promise<unknown>): Promise<string> {
	const chunks: string[] = [];
	const orig = process.stderr.write.bind(process.stderr);
	(process.stderr as NodeJS.WriteStream).write = (chunk: unknown) => {
		chunks.push(String(chunk));
		return true;
	};
	try {
		await fn();
	} finally {
		(process.stderr as NodeJS.WriteStream).write = orig;
	}
	return chunks.join("");
}

// ---------------------------------------------------------------------------
// maskKeyLast4 unit tests
// ---------------------------------------------------------------------------

describe("maskKeyLast4", () => {
	test("masks all but last 4 chars with *** prefix", () => {
		assert.equal(maskKeyLast4("tp-newkey12345"), "***2345");
	});

	test("handles key exactly 4 chars long", () => {
		assert.equal(maskKeyLast4("abcd"), "***abcd");
	});

	test("handles key shorter than 4 chars", () => {
		assert.equal(maskKeyLast4("abc"), "***abc");
	});

	test("returns (empty) for falsy input", () => {
		assert.equal(maskKeyLast4(""), "(empty)");
	});

	test("never exposes more than last 4 chars of a long key", () => {
		const key = "sk-ant-api-xxxxxxxxxxxxxxxxxxxxxxxxxxxx1234";
		const masked = maskKeyLast4(key);
		assert.ok(masked.startsWith("***"), "must start with ***");
		assert.ok(!masked.includes("xxxx"), "must not expose middle");
		assert.ok(masked.endsWith("1234"), "must end with last 4");
	});
});

// ---------------------------------------------------------------------------
// sf key set
// ---------------------------------------------------------------------------

describe("sf key set", () => {
	test("sets a new key and reports masked old+new", async () => {
		const authPath = makeTempAuthPath();
		let out = "";
		out = await captureStdout(async () => {
			const code = await runKeyCommand(authPath, [
				"set",
				"xiaomi",
				"tp-newrotation-12345",
			]);
			assert.equal(code, 0);
		});
		assert.ok(out.includes("Updated xiaomi credential"), `output: ${out}`);
		assert.ok(
			out.includes("(none)"),
			"old key should be (none) when not previously set",
		);
		assert.ok(out.includes("***2345"), "should show last 4 of new key");
		assert.ok(!out.includes("tp-newrotation-12345"), "must not echo full key");
	});

	test("shows old masked key when rotating", async () => {
		const authPath = makeTempAuthPath();
		// Set initial key
		await runKeyCommand(authPath, ["set", "xiaomi", "tp-first-key-aaaa"]);
		// Rotate
		let out = "";
		out = await captureStdout(async () => {
			const code = await runKeyCommand(authPath, [
				"set",
				"xiaomi",
				"tp-second-key-bbbb",
			]);
			assert.equal(code, 0);
		});
		assert.ok(out.includes("***aaaa"), "old masked key should be shown");
		assert.ok(out.includes("***bbbb"), "new masked key should be shown");
	});

	test("rejects an unknown provider with exit code 1", async () => {
		const authPath = makeTempAuthPath();
		let err = "";
		err = await captureStderr(async () => {
			const code = await runKeyCommand(authPath, [
				"set",
				"unknownprovider",
				"foo-key",
			]);
			assert.equal(code, 1);
		});
		assert.ok(err.includes("Unknown provider"), `stderr: ${err}`);
		assert.ok(
			err.includes("unknownprovider"),
			`stderr should echo the bad name: ${err}`,
		);
		assert.ok(err.includes("Valid providers"), "should list valid providers");
	});

	test("rejects empty key with exit code 1", async () => {
		const authPath = makeTempAuthPath();
		let err = "";
		let exitCode = -1;
		err = await captureStderr(async () => {
			exitCode = await runKeyCommand(authPath, ["set", "xiaomi", ""]);
		});
		assert.equal(exitCode, 1);
		assert.ok(err.includes("non-empty"), `stderr: ${err}`);
	});

	test("missing args returns exit code 1", async () => {
		const authPath = makeTempAuthPath();
		let err = "";
		let exitCode = -1;
		err = await captureStderr(async () => {
			exitCode = await runKeyCommand(authPath, ["set", "xiaomi"]);
		});
		assert.equal(exitCode, 1);
		assert.ok(err.includes("Usage"), `stderr: ${err}`);
	});
});

// ---------------------------------------------------------------------------
// sf key get
// ---------------------------------------------------------------------------

describe("sf key get", () => {
	test("shows last-4 chars only when key is set", async () => {
		const authPath = makeTempAuthPath();
		await runKeyCommand(authPath, ["set", "xiaomi", "tp-some-long-key-wxyz"]);

		let out = "";
		out = await captureStdout(async () => {
			const code = await runKeyCommand(authPath, ["get", "xiaomi"]);
			assert.equal(code, 0);
		});
		assert.ok(out.includes("api_key set"), `output: ${out}`);
		assert.ok(out.includes("***wxyz"), `should show last 4: ${out}`);
		assert.ok(!out.includes("tp-some-long-key-wxyz"), "must not echo full key");
	});

	test("shows 'no credential set' when provider not configured", async () => {
		const authPath = makeTempAuthPath();
		let out = "";
		out = await captureStdout(async () => {
			const code = await runKeyCommand(authPath, ["get", "xiaomi"]);
			assert.equal(code, 0);
		});
		assert.ok(out.includes("no credential set"), `output: ${out}`);
	});

	test("rejects unknown provider with exit code 1", async () => {
		const authPath = makeTempAuthPath();
		let err = "";
		err = await captureStderr(async () => {
			const code = await runKeyCommand(authPath, ["get", "bogusprovider"]);
			assert.equal(code, 1);
		});
		assert.ok(err.includes("Unknown provider"), `stderr: ${err}`);
	});
});

// ---------------------------------------------------------------------------
// sf key remove
// ---------------------------------------------------------------------------

describe("sf key remove", () => {
	test("removes credential with --yes flag (no prompt)", async () => {
		const authPath = makeTempAuthPath();
		await runKeyCommand(authPath, ["set", "xiaomi", "tp-key-to-remove-zzzz"]);

		let out = "";
		out = await captureStdout(async () => {
			const code = await runKeyCommand(authPath, ["remove", "xiaomi", "--yes"]);
			assert.equal(code, 0);
		});
		assert.ok(out.includes("Removed"), `output: ${out}`);

		// Verify key is gone
		let getOut = "";
		getOut = await captureStdout(async () => {
			await runKeyCommand(authPath, ["get", "xiaomi"]);
		});
		assert.ok(getOut.includes("no credential set"), `after remove: ${getOut}`);
	});

	test("returns 1 when provider has no credential", async () => {
		const authPath = makeTempAuthPath();
		let err = "";
		let exitCode = -1;
		err = await captureStderr(async () => {
			exitCode = await runKeyCommand(authPath, ["remove", "xiaomi", "--yes"]);
		});
		assert.equal(exitCode, 1);
		assert.ok(err.includes("No credential"), `stderr: ${err}`);
	});

	test("rejects unknown provider with exit code 1", async () => {
		const authPath = makeTempAuthPath();
		const code = await runKeyCommand(authPath, ["remove", "notreal", "--yes"]);
		assert.equal(code, 1);
	});

	test("rm alias works the same as remove", async () => {
		const authPath = makeTempAuthPath();
		await runKeyCommand(authPath, ["set", "xiaomi", "tp-alias-test-1234"]);
		let out = "";
		out = await captureStdout(async () => {
			const code = await runKeyCommand(authPath, ["rm", "xiaomi", "--yes"]);
			assert.equal(code, 0);
		});
		assert.ok(out.includes("Removed"), `rm alias output: ${out}`);
	});
});

// ---------------------------------------------------------------------------
// sf key list
// ---------------------------------------------------------------------------

describe("sf key list", () => {
	test("shows xiaomi as set after key is added", async () => {
		const authPath = makeTempAuthPath();
		await runKeyCommand(authPath, ["set", "xiaomi", "tp-list-test-1234"]);

		let out = "";
		out = await captureStdout(async () => {
			const code = await runKeyCommand(authPath, ["list"]);
			assert.equal(code, 0);
		});
		assert.ok(out.includes("xiaomi"), `output should include xiaomi: ${out}`);
		assert.ok(out.includes("set"), `xiaomi should be marked set: ${out}`);
		assert.ok(out.includes("***1234"), `should show last 4: ${out}`);
		assert.ok(!out.includes("tp-list-test-1234"), "must not echo full key");
	});

	test("shows 'not set' for providers with no credential", async () => {
		const authPath = makeTempAuthPath();
		let out = "";
		out = await captureStdout(async () => {
			await runKeyCommand(authPath, ["list"]);
		});
		assert.ok(
			out.includes("not set"),
			`some providers should show not set: ${out}`,
		);
	});

	test("ls alias works", async () => {
		const authPath = makeTempAuthPath();
		let out = "";
		out = await captureStdout(async () => {
			const code = await runKeyCommand(authPath, ["ls"]);
			assert.equal(code, 0);
		});
		assert.ok(out.includes("Provider credentials"), `ls alias: ${out}`);
	});
});

// ---------------------------------------------------------------------------
// sf key sync
// ---------------------------------------------------------------------------

/**
 * Wipe all env vars that correspond to PROVIDER_REGISTRY envVar fields,
 * run the callback, then restore. Ensures sync tests are isolated from the
 * developer's real environment (CI boxes often have many keys set).
 */
async function withCleanProviderEnv(fn: () => Promise<void>): Promise<void> {
	const KNOWN_ENV_VARS = [
		"ANTHROPIC_API_KEY",
		"OPENAI_API_KEY",
		"GROQ_API_KEY",
		"OPENROUTER_API_KEY",
		"MISTRAL_API_KEY",
		"MINIMAX_API_KEY",
		"KIMI_API_KEY",
		"ZAI_API_KEY",
		"XIAOMI_API_KEY",
		"OLLAMA_CLOUD_API_KEY",
		"OLLAMA_API_KEY",
		"OPENCODE_API_KEY",
		"OPENCODE_GO_API_KEY",
		"CUSTOM_OPENAI_API_KEY",
		"CEREBRAS_API_KEY",
		"AZURE_OPENAI_API_KEY",
		"ALIBABA_API_KEY",
		"DASHSCOPE_API_KEY",
		"CONTEXT7_API_KEY",
		"JINA_API_KEY",
		"TAVILY_API_KEY",
		"BRAVE_API_KEY",
		"SERPER_API_KEY",
		"EXA_API_KEY",
		"DISCORD_BOT_TOKEN",
		"SLACK_BOT_TOKEN",
		"TELEGRAM_BOT_TOKEN",
	];
	const saved: Record<string, string | undefined> = {};
	for (const k of KNOWN_ENV_VARS) {
		saved[k] = process.env[k];
		delete process.env[k];
	}
	try {
		await fn();
	} finally {
		for (const k of KNOWN_ENV_VARS) {
			if (saved[k] !== undefined) {
				process.env[k] = saved[k];
			} else {
				delete process.env[k];
			}
		}
	}
}

describe("sf key sync", () => {
	test("rotates stored key when env var differs", async () => {
		await withCleanProviderEnv(async () => {
			const authPath = makeTempAuthPath();
			// Store an old key
			await runKeyCommand(authPath, ["set", "xiaomi", "tp-old-key-oldval"]);
			// Set env var to a different value
			process.env.XIAOMI_API_KEY = "tp-env-key-newval";

			let out = "";
			out = await captureStdout(async () => {
				const code = await runKeyCommand(authPath, ["sync"]);
				assert.equal(code, 0);
			});
			assert.ok(
				out.includes("Rotated xiaomi"),
				`should report rotation: ${out}`,
			);
			assert.ok(
				out.includes("XIAOMI_API_KEY"),
				`should mention env var name: ${out}`,
			);

			// Confirm the stored key was updated
			let getOut = "";
			getOut = await captureStdout(async () => {
				await runKeyCommand(authPath, ["get", "xiaomi"]);
			});
			assert.ok(
				getOut.includes("***wval"),
				`should now show env key's last 4: ${getOut}`,
			);
		});
	});

	test("reports no changes when env matches stored key", async () => {
		await withCleanProviderEnv(async () => {
			const authPath = makeTempAuthPath();
			await runKeyCommand(authPath, ["set", "xiaomi", "tp-same-key-same"]);
			process.env.XIAOMI_API_KEY = "tp-same-key-same";

			let out = "";
			out = await captureStdout(async () => {
				const code = await runKeyCommand(authPath, ["sync"]);
				assert.equal(code, 0);
			});
			assert.ok(out.includes("No changes"), `should report no changes: ${out}`);
		});
	});

	test("skips providers with no env var set", async () => {
		await withCleanProviderEnv(async () => {
			const authPath = makeTempAuthPath();
			await runKeyCommand(authPath, ["set", "xiaomi", "tp-stored-key-xxxx"]);
			// XIAOMI_API_KEY is already cleared by withCleanProviderEnv

			let out = "";
			out = await captureStdout(async () => {
				const code = await runKeyCommand(authPath, ["sync"]);
				assert.equal(code, 0);
			});
			// xiaomi should NOT be rotated since env is not set
			assert.ok(
				!out.includes("Rotated xiaomi"),
				`should skip xiaomi when env not set: ${out}`,
			);
			assert.ok(out.includes("No changes"), `should report no changes: ${out}`);
		});
	});
});

// ---------------------------------------------------------------------------
// sf key — unknown subcommand
// ---------------------------------------------------------------------------

describe("sf key unknown subcommand", () => {
	test("returns exit code 1 with error message", async () => {
		const authPath = makeTempAuthPath();
		let err = "";
		err = await captureStderr(async () => {
			const code = await runKeyCommand(authPath, ["boguscmd"]);
			assert.equal(code, 1);
		});
		assert.ok(err.includes("Unknown subcommand"), `stderr: ${err}`);
	});
});

// ---------------------------------------------------------------------------
// cli.ts dispatch contract (static analysis)
// ---------------------------------------------------------------------------

describe("cli.ts dispatch contract", () => {
	test("sf key branch exists before InteractiveMode in cli.ts", () => {
		const { readFileSync } = require("node:fs");
		const { join } = require("node:path");
		const source = readFileSync(join(__dirname, "..", "cli.ts"), "utf-8");

		const keyBranch = source.indexOf('cliFlags.messages[0] === "key"');
		const interactiveMode = source.indexOf("new InteractiveMode");

		assert.notEqual(
			keyBranch,
			-1,
			"sf key dispatch branch must exist in cli.ts",
		);
		assert.notEqual(
			interactiveMode,
			-1,
			"InteractiveMode must exist in cli.ts",
		);
		assert.ok(
			keyBranch < interactiveMode,
			"sf key must route before the interactive TUI path",
		);
		assert.ok(
			source.includes("runKeyCommand"),
			"cli.ts must call runKeyCommand",
		);
	});

	test("key subcommand help entry exists in help-text.ts", () => {
		const { readFileSync } = require("node:fs");
		const { join } = require("node:path");
		const source = readFileSync(join(__dirname, "..", "help-text.ts"), "utf-8");

		assert.ok(
			source.includes('"key":') || source.includes("key:"),
			"key help entry must exist",
		);
		assert.ok(source.includes("sf key"), "help text must mention sf key");
	});
});
