import assert from "node:assert/strict";
import { test } from "vitest";

import { setupEditorSubmitHandler } from "./input-controller.js";

type HostOptions = {
	knownSlashCommands?: string[];
	slashCommandContext?: boolean;
};

function getSlashCommandName(text: string): string {
	const trimmed = text.trim();
	const spaceIndex = trimmed.indexOf(" ");
	return spaceIndex === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIndex);
}

function createHost(options: HostOptions = {}) {
	const prompted: string[] = [];
	const promptOptions: unknown[] = [];
	const errors: string[] = [];
	const warnings: string[] = [];
	const history: string[] = [];
	const knownSlashCommands = new Set(options.knownSlashCommands ?? []);
	let editorText = "";
	let settingsOpened = 0;
	let aborts = 0;
	let shutdowns = 0;
	const statuses: string[] = [];
	let pendingDisplayUpdates = 0;
	let renderRequests = 0;

	const editor = {
		setText(text: string) {
			editorText = text;
		},
		getText() {
			return editorText;
		},
		addToHistory(text: string) {
			history.push(text);
		},
	};

	const host = {
		defaultEditor: editor as typeof editor & {
			onSubmit?: (text: string) => Promise<void>;
		},
		editor,
		session: {
			isBashRunning: false,
			isCompacting: false,
			isStreaming: false,
			prompt: async (text: string, options?: unknown) => {
				prompted.push(text);
				promptOptions.push(options);
			},
			abort: async () => {
				aborts += 1;
			},
		},
		ui: {
			requestRender() {
				renderRequests += 1;
			},
		},
		...(options.slashCommandContext === false
			? {}
			: {
					getSlashCommandContext: () => ({
						session: host.session,
						showSettingsSelector: () => {
							settingsOpened += 1;
						},
						showStatus: host.showStatus,
						shutdown: async () => {
							shutdowns += 1;
						},
					}),
				}),
		handleBashCommand: async () => {},
		showWarning(message: string) {
			warnings.push(message);
		},
		showError(message: string) {
			errors.push(message);
		},
		showStatus(message: string) {
			statuses.push(message);
		},
		updateEditorBorderColor() {},
		isExtensionCommand() {
			return false;
		},
		isKnownSlashCommand(text: string) {
			return knownSlashCommands.has(getSlashCommandName(text));
		},
		queueCompactionMessage() {},
		updatePendingMessagesDisplay() {
			pendingDisplayUpdates += 1;
		},
		contextualTips: {
			evaluate: () => undefined,
			recordBashIncluded() {},
		},
		getContextPercent: () => undefined,
	};

	setupEditorSubmitHandler(host as any);

	return {
		host: host as typeof host & {
			defaultEditor: typeof editor & {
				onSubmit: (text: string) => Promise<void>;
			};
		},
		prompted,
		promptOptions,
		errors,
		warnings,
		history,
		getEditorText: () => editorText,
		getSettingsOpened: () => settingsOpened,
		getAborts: () => aborts,
		getShutdowns: () => shutdowns,
		statuses,
		getPendingDisplayUpdates: () => pendingDisplayUpdates,
		getRenderRequests: () => renderRequests,
	};
}

test("input-controller: built-in slash commands stay in TUI dispatch", async () => {
	const { host, prompted, errors, getSettingsOpened, getEditorText } =
		createHost();

	await host.defaultEditor.onSubmit("/settings");

	assert.equal(
		getSettingsOpened(),
		1,
		"built-in /settings should open the settings selector",
	);
	assert.deepEqual(
		prompted,
		[],
		"built-in slash commands should not reach session.prompt",
	);
	assert.deepEqual(
		errors,
		[],
		"built-in slash commands should not show errors",
	);
	assert.equal(
		getEditorText(),
		"",
		"built-in slash commands should clear the editor after handling",
	);
});

test("input-controller: /exit is a built-in shutdown alias", async () => {
	const { host, prompted, errors, getEditorText, getShutdowns } = createHost();

	await host.defaultEditor.onSubmit("/exit");

	assert.equal(getShutdowns(), 1);
	assert.deepEqual(prompted, []);
	assert.deepEqual(errors, []);
	assert.equal(getEditorText(), "");
});

test("input-controller: /stop aborts the current response", async () => {
	const { host, prompted, errors, statuses, getAborts, getEditorText } =
		createHost();

	await host.defaultEditor.onSubmit("/stop");

	assert.equal(getAborts(), 1);
	assert.deepEqual(prompted, []);
	assert.deepEqual(errors, []);
	assert.deepEqual(statuses, ["Stopped current response."]);
	assert.equal(getEditorText(), "");
});

test("input-controller: extension slash commands fall through to session.prompt", async () => {
	const { host, prompted, errors, history } = createHost({
		knownSlashCommands: ["sf"],
	});

	await host.defaultEditor.onSubmit("/sf help");

	assert.deepEqual(
		prompted,
		["/sf help"],
		"known extension slash commands should reach session.prompt",
	);
	assert.deepEqual(
		errors,
		[],
		"known extension slash commands should not show unknown-command errors",
	);
	assert.deepEqual(
		history,
		["/sf help"],
		"known extension slash commands should still be added to history",
	);
});

test("input-controller: prompt template slash commands fall through to session.prompt", async () => {
	const { host, prompted, errors } = createHost({
		knownSlashCommands: ["daily"],
	});

	await host.defaultEditor.onSubmit("/daily focus area");

	assert.deepEqual(prompted, ["/daily focus area"]);
	assert.deepEqual(errors, []);
});

test("input-controller: known extension slash command falls through when slash context is absent", async () => {
	const { host, prompted, errors, history } = createHost({
		knownSlashCommands: ["sf"],
		slashCommandContext: false,
	});

	await host.defaultEditor.onSubmit("/sf next");

	assert.deepEqual(prompted, ["/sf next"]);
	assert.deepEqual(errors, []);
	assert.deepEqual(history, ["/sf next"]);
});

test("input-controller: built-in slash command does not crash when slash context is absent", async () => {
	const { host, prompted, errors, getSettingsOpened, getEditorText } =
		createHost({
			slashCommandContext: false,
		});

	await host.defaultEditor.onSubmit("/settings");

	assert.equal(getSettingsOpened(), 0);
	assert.deepEqual(prompted, []);
	assert.deepEqual(errors, [
		"Unknown command: /settings. Use slash autocomplete to see available commands.",
	]);
	assert.equal(getEditorText(), "");
});

test("input-controller: normal prompt submit does not require stale bash component flush hook", async () => {
	const { host, prompted, errors, history } = createHost();

	await host.defaultEditor.onSubmit("ping");

	assert.deepEqual(prompted, ["ping"]);
	assert.deepEqual(errors, []);
	assert.deepEqual(history, ["ping"]);
});

test("input-controller: skill slash commands fall through to session.prompt", async () => {
	const { host, prompted, errors } = createHost({
		knownSlashCommands: ["skill:create-skill"],
	});

	await host.defaultEditor.onSubmit("/skill:create-skill routing bug");

	assert.deepEqual(prompted, ["/skill:create-skill routing bug"]);
	assert.deepEqual(errors, []);
});

test("input-controller: disabled skill slash commands stay unknown", async () => {
	const { host, prompted, errors } = createHost();

	await host.defaultEditor.onSubmit("/skill:create-skill routing bug");

	assert.deepEqual(prompted, []);
	assert.deepEqual(errors, [
		"Unknown command: /skill:create-skill. Use slash autocomplete to see available commands.",
	]);
});

test("input-controller: /export prefix does not swallow unrelated slash commands", async () => {
	const { host, prompted, errors } = createHost();

	await host.defaultEditor.onSubmit("/exportfoo");

	assert.deepEqual(prompted, []);
	assert.deepEqual(errors, [
		"Unknown command: /exportfoo. Use slash autocomplete to see available commands.",
	]);
});

test("input-controller: truly unknown slash commands stop before session.prompt", async () => {
	const { host, prompted, errors, getEditorText } = createHost();

	await host.defaultEditor.onSubmit("/definitely-not-a-command");

	assert.deepEqual(
		prompted,
		[],
		"unknown slash commands should not reach session.prompt",
	);
	assert.deepEqual(errors, [
		"Unknown command: /definitely-not-a-command. Use slash autocomplete to see available commands.",
	]);
	assert.equal(
		getEditorText(),
		"",
		"unknown slash commands should clear the editor after showing the error",
	);
});

test("input-controller: absolute file paths are not treated as slash commands (#3478)", async () => {
	const { host, prompted, errors } = createHost();

	await host.defaultEditor.onSubmit("/Users/name/Desktop/screenshot.png");

	assert.deepEqual(
		errors,
		[],
		"file paths should not trigger unknown command error",
	);
	assert.deepEqual(
		prompted,
		["/Users/name/Desktop/screenshot.png"],
		"file paths should be sent as plain input",
	);
});

test("input-controller: Linux absolute paths are not treated as slash commands (#3478)", async () => {
	const { host, prompted, errors } = createHost();

	await host.defaultEditor.onSubmit("/home/user/documents/file.txt");

	assert.deepEqual(
		errors,
		[],
		"Linux paths should not trigger unknown command error",
	);
	assert.deepEqual(
		prompted,
		["/home/user/documents/file.txt"],
		"Linux paths should be sent as plain input",
	);
});

test("input-controller: /tmp paths are not treated as slash commands (#3478)", async () => {
	const { host, prompted, errors } = createHost();

	await host.defaultEditor.onSubmit("/tmp/some-file.log");

	assert.deepEqual(errors, []);
	assert.deepEqual(prompted, ["/tmp/some-file.log"]);
});

test("input-controller: dot aborts streaming instead of steering", async () => {
	const {
		host,
		prompted,
		history,
		getAborts,
		getEditorText,
		getPendingDisplayUpdates,
		getRenderRequests,
	} = createHost();
	host.session.isStreaming = true;

	await host.defaultEditor.onSubmit(".");

	assert.equal(getAborts(), 1, "dot should abort the active stream");
	assert.deepEqual(prompted, [], "dot should not be sent as a steering prompt");
	assert.deepEqual(history, ["."], "dot abort should remain in input history");
	assert.equal(getEditorText(), "", "dot abort should clear the editor");
	assert.equal(getPendingDisplayUpdates(), 1);
	assert.equal(getRenderRequests(), 1);
});

test("input-controller: normal input while streaming is buffered as steering", async () => {
	const {
		host,
		prompted,
		promptOptions,
		history,
		getAborts,
		getEditorText,
		getPendingDisplayUpdates,
		getRenderRequests,
	} = createHost();
	host.session.isStreaming = true;

	await host.defaultEditor.onSubmit("use the simpler parser");

	assert.equal(getAborts(), 0, "normal streaming input must not abort");
	assert.deepEqual(prompted, ["use the simpler parser"]);
	assert.deepEqual(promptOptions, [{ streamingBehavior: "steer" }]);
	assert.deepEqual(history, ["use the simpler parser"]);
	assert.equal(
		getEditorText(),
		"",
		"streaming steering should clear the editor",
	);
	assert.equal(getPendingDisplayUpdates(), 1);
	assert.equal(getRenderRequests(), 1);
});
