import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "vitest";
import { AuthStorage } from "../auth-storage.js";
import { ModelRegistry } from "../model-registry.js";
import { SessionManager } from "../session-manager.js";
import type { Extension, ExtensionRuntime, ToolCallEvent } from "./index.js";
import { ExtensionRunner } from "./runner.js";

function makeMinimalRuntime(): ExtensionRuntime {
	return {
		sendMessage: async () => {},
		sendUserMessage: async () => {},
		appendEntry: () => {},
		setSessionName: () => {},
		getSessionName: () => undefined,
		setLabel: () => {},
		getActiveTools: () => [],
		getAllTools: () => [],
		setActiveTools: () => {},
		refreshTools: () => {},
		getCommands: () => [],
		setModel: async () => {},
		getThinkingLevel: () => undefined,
		setThinkingLevel: () => {},
		registerProvider: () => {},
		unregisterProvider: () => {},
		pendingProviderRegistrations: [],
	} as unknown as ExtensionRuntime;
}

function makeThrowingExtension(eventType: string, error: Error): Extension {
	const handlers = new Map();
	handlers.set(eventType, [
		async () => {
			throw error;
		},
	]);
	return {
		path: "/test/throwing-ext",
		handlers,
		commands: [],
		shortcuts: [],
		diagnostics: [],
	} as unknown as Extension;
}

describe("ExtensionRunner.emitToolCall", () => {
	it("catches throwing extension handler and routes to emitError", async () => {
		const dir = mkdtempSync(join(tmpdir(), "runner-test-"));
		try {
			const sessionManager = SessionManager.create(dir, dir);
			const authStorage = AuthStorage.create();
			const modelRegistry = new ModelRegistry(
				authStorage,
				join(dir, "models.json"),
			);

			const throwingExt = makeThrowingExtension(
				"tool_call",
				new Error("handler crashed"),
			);
			const runtime = makeMinimalRuntime();
			const runner = new ExtensionRunner(
				[throwingExt],
				runtime,
				dir,
				sessionManager,
				modelRegistry,
			);

			const errors: any[] = [];
			runner.onError((err) => errors.push(err));

			const event: ToolCallEvent = {
				type: "tool_call",
				toolCallId: "test-123",
				toolName: "test_tool",
				input: {},
			} as ToolCallEvent;

			const result = await runner.emitToolCall(event);

			// Should not throw — error is caught and routed to emitError
			assert.equal(result, undefined);
			assert.equal(errors.length, 1);
			assert.equal(errors[0].error, "handler crashed");
			assert.equal(errors[0].event, "tool_call");
			assert.equal(errors[0].extensionPath, "/test/throwing-ext");
		} finally {
			rmSync(dir, { recursive: true, force: true });
		}
	});
});

describe("ExtensionRunner UI compatibility", () => {
	it("set_widget_when_host_widget_method_throws_does_not_fail_extension_event", async () => {
		const dir = mkdtempSync(join(tmpdir(), "runner-widget-test-"));
		try {
			const sessionManager = SessionManager.create(dir, dir);
			const authStorage = AuthStorage.create();
			const modelRegistry = new ModelRegistry(
				authStorage,
				join(dir, "models.json"),
			);
			const handlers = new Map();
			handlers.set("session_start", [
				async (_event: unknown, ctx: any) => {
					ctx.ui.setWidget("sf-progress", ["ready"], {
						placement: "belowEditor",
					});
				},
			]);
			const extension = {
				path: "/test/widget-ext",
				handlers,
				commands: [],
				shortcuts: [],
				diagnostics: [],
			} as unknown as Extension;
			const runner = new ExtensionRunner(
				[extension],
				makeMinimalRuntime(),
				dir,
				sessionManager,
				modelRegistry,
			);
			const errors: any[] = [];
			runner.onError((err) => errors.push(err));
			runner.setUIContext({
				...runner.getUIContext(),
				setWidget: () => {
					throw new TypeError("host.setExtensionWidget is not a function");
				},
			});

			await runner.emit({ type: "session_start" });

			assert.deepEqual(errors, []);
		} finally {
			rmSync(dir, { recursive: true, force: true });
		}
	});
});

describe("ExtensionRunner command conflicts", () => {
	it("registered_command_when_reserved_for_builtin_delegation_is_hidden_without_warning", () => {
		const dir = mkdtempSync(join(tmpdir(), "runner-command-test-"));
		try {
			const sessionManager = SessionManager.create(dir, dir);
			const authStorage = AuthStorage.create();
			const modelRegistry = new ModelRegistry(
				authStorage,
				join(dir, "models.json"),
			);
			const commands = new Map();
			commands.set("exit", {
				name: "exit",
				description: "Graceful extension exit",
				handler: async () => undefined,
			});
			const extension = {
				path: "/test/sf-ext",
				handlers: new Map(),
				commands,
				shortcuts: [],
				diagnostics: [],
			} as unknown as Extension;
			const runner = new ExtensionRunner(
				[extension],
				makeMinimalRuntime(),
				dir,
				sessionManager,
				modelRegistry,
			);

			const commandsForAutocomplete = runner.getRegisteredCommands(
				new Set(["exit"]),
				new Set(["exit"]),
			);

			assert.deepEqual(commandsForAutocomplete, []);
			assert.deepEqual(runner.getCommandDiagnostics(), []);
			assert.equal(
				runner.getCommand("exit")?.description,
				"Graceful extension exit",
			);
		} finally {
			rmSync(dir, { recursive: true, force: true });
		}
	});

	it("registered_command_when_reserved_without_delegation_emits_conflict_warning", () => {
		const dir = mkdtempSync(join(tmpdir(), "runner-command-test-"));
		try {
			const sessionManager = SessionManager.create(dir, dir);
			const authStorage = AuthStorage.create();
			const modelRegistry = new ModelRegistry(
				authStorage,
				join(dir, "models.json"),
			);
			const commands = new Map();
			commands.set("reload", {
				name: "reload",
				description: "Duplicate reload",
				handler: async () => undefined,
			});
			const extension = {
				path: "/test/duplicate-ext",
				handlers: new Map(),
				commands,
				shortcuts: [],
				diagnostics: [],
			} as unknown as Extension;
			const runner = new ExtensionRunner(
				[extension],
				makeMinimalRuntime(),
				dir,
				sessionManager,
				modelRegistry,
			);

			assert.deepEqual(runner.getRegisteredCommands(new Set(["reload"])), []);
			assert.equal(runner.getCommandDiagnostics().length, 1);
			assert.match(
				runner.getCommandDiagnostics()[0]?.message ?? "",
				/conflicts with built-in commands/,
			);
		} finally {
			rmSync(dir, { recursive: true, force: true });
		}
	});
});
