/**
 * commands-workflow-custom.test.ts — Tests for `/sf workflow` subcommands
 * and catalog completions.
 *
 * Uses real temp directories with actual definition YAML files.
 */

import assert from "node:assert/strict";
import { execSync } from "node:child_process";
import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readFileSync,
	readdirSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, before, describe, it } from 'vitest';

import {
	getSfArgumentCompletions,
	TOP_LEVEL_SUBCOMMANDS,
} from "../commands/catalog.ts";
import { loadRegistry } from "../workflow-templates.ts";

// ─── Helpers ─────────────────────────────────────────────────────────────

const tmpDirs: string[] = [];
let savedCwd: string;

function makeTmpBase(): string {
	const dir = mkdtempSync(join(tmpdir(), "wf-cmd-test-"));
	tmpDirs.push(dir);
	return dir;
}

function initGitRepo(base: string): void {
	execSync("git init", { cwd: base, stdio: "ignore" });
	execSync("git config user.email test@example.com", {
		cwd: base,
		stdio: "ignore",
	});
	execSync("git config user.name Test", { cwd: base, stdio: "ignore" });
}

afterEach(() => {
	// Restore cwd if changed during tests
	if (savedCwd && process.cwd() !== savedCwd) {
		process.chdir(savedCwd);
	}
	for (const d of tmpDirs) {
		try {
			rmSync(d, {
				recursive: true,
				force: true,
				maxRetries: 3,
				retryDelay: 100,
			});
		} catch {
			/* Windows EPERM */
		}
	}
	tmpDirs.length = 0;
});

beforeAll(() => {
	savedCwd = process.cwd();
});

function createMockCtx() {
	const notifications: { message: string; level: string }[] = [];
	return {
		notifications,
		ui: {
			notify(message: string, level: string) {
				notifications.push({ message, level });
			},
			custom: async () => {},
		},
		shutdown: async () => {},
		sessionManager: {
			getSessionFile: () => null,
		},
	};
}

function createMockPi() {
	const messages: unknown[] = [];
	return {
		messages,
		registerCommand() {},
		registerTool() {},
		registerShortcut() {},
		on() {},
		sendMessage(message: unknown) {
			messages.push(message);
		},
	};
}

/** Write a minimal valid workflow definition YAML to the expected location. */
function writeDefinition(
	basePath: string,
	name: string,
	content: string,
): void {
	const defsDir = join(basePath, ".sf", "workflow-defs");
	mkdirSync(defsDir, { recursive: true });
	writeFileSync(join(defsDir, `${name}.yaml`), content, "utf-8");
}

const SIMPLE_DEF = `
version: 1
name: test-workflow
description: A test workflow
steps:
  - id: step-1
    name: First Step
    prompt: Do step 1
    requires: []
    produces: []
`;

const _INVALID_DEF = `
version: 2
name: bad-workflow
steps: []
`;

// ─── Catalog Registration ────────────────────────────────────────────────

describe("workflow catalog registration", () => {
	it("model appears in TOP_LEVEL_SUBCOMMANDS", () => {
		const entry = TOP_LEVEL_SUBCOMMANDS.find((c) => c.cmd === "model");
		assert.ok(entry, "model should be in TOP_LEVEL_SUBCOMMANDS");
		assert.match(entry!.desc, /session model/i);
	});

	it("getSfArgumentCompletions('m') includes model", () => {
		const completions = getSfArgumentCompletions("m");
		const labels = completions.map((c: any) => c.label);
		assert.ok(labels.includes("model"), "should include model completion");
	});

	it("autonomous appears in TOP_LEVEL_SUBCOMMANDS and auto remains an alias", () => {
		const autonomous = TOP_LEVEL_SUBCOMMANDS.find(
			(c) => c.cmd === "autonomous",
		);
		const auto = TOP_LEVEL_SUBCOMMANDS.find((c) => c.cmd === "auto");
		assert.ok(autonomous, "autonomous should be in TOP_LEVEL_SUBCOMMANDS");
		assert.match(autonomous!.desc, /Autonomous mode/i);
		assert.ok(auto, "auto alias should remain in TOP_LEVEL_SUBCOMMANDS");
		assert.match(auto!.desc, /alias/i);
	});

	it("getSfArgumentCompletions supports autonomous flags", () => {
		const completions = getSfArgumentCompletions("autonomous ");
		const labels = completions.map((c: any) => c.label);
		assert.ok(labels.includes("--verbose"), "should include verbose flag");
		assert.ok(labels.includes("--debug"), "should include debug flag");
	});

	it("workflow appears in TOP_LEVEL_SUBCOMMANDS", () => {
		const entry = TOP_LEVEL_SUBCOMMANDS.find((c) => c.cmd === "workflow");
		assert.ok(entry, "workflow should be in TOP_LEVEL_SUBCOMMANDS");
		assert.ok(entry!.desc.includes("new"), "description should mention new");
		assert.ok(entry!.desc.includes("run"), "description should mention run");
	});

	it("getSfArgumentCompletions('workflow ') returns six subcommands", () => {
		const completions = getSfArgumentCompletions("workflow ");
		const labels = completions.map((c: any) => c.label);
		for (const sub of ["new", "run", "list", "validate", "pause", "resume"]) {
			assert.ok(labels.includes(sub), `missing completion: ${sub}`);
		}
		assert.equal(labels.length, 6, "should have exactly 6 subcommands");
	});

	it("getSfArgumentCompletions('workflow r') filters to run and resume", () => {
		const completions = getSfArgumentCompletions("workflow r");
		const labels = completions.map((c: any) => c.label);
		assert.ok(labels.includes("run"), "should include run");
		assert.ok(labels.includes("resume"), "should include resume");
		assert.ok(!labels.includes("list"), "should not include list");
	});

	it("getSfArgumentCompletions('workflow run ') returns definition names", () => {
		const base = makeTmpBase();
		writeDefinition(base, "deploy-pipeline", SIMPLE_DEF);
		writeDefinition(base, "test-suite", SIMPLE_DEF);

		// Change cwd so the completion scanner can find `.sf/workflow-defs/`
		process.chdir(base);

		const completions = getSfArgumentCompletions("workflow run ");
		const labels = completions.map((c: any) => c.label);
		assert.ok(
			labels.includes("deploy-pipeline"),
			"should include deploy-pipeline",
		);
		assert.ok(labels.includes("test-suite"), "should include test-suite");
	});

	it("getSfArgumentCompletions('workflow validate ') returns definition names", () => {
		const base = makeTmpBase();
		writeDefinition(base, "my-workflow", SIMPLE_DEF);

		process.chdir(base);

		const completions = getSfArgumentCompletions("workflow validate ");
		const labels = completions.map((c: any) => c.label);
		assert.ok(labels.includes("my-workflow"), "should include my-workflow");
	});

	it("getSfArgumentCompletions('workflow run d') filters by prefix", () => {
		const base = makeTmpBase();
		writeDefinition(base, "deploy-pipeline", SIMPLE_DEF);
		writeDefinition(base, "test-suite", SIMPLE_DEF);

		process.chdir(base);

		const completions = getSfArgumentCompletions("workflow run d");
		const labels = completions.map((c: any) => c.label);
		assert.ok(
			labels.includes("deploy-pipeline"),
			"should include deploy-pipeline",
		);
		assert.ok(!labels.includes("test-suite"), "should not include test-suite");
	});

	it("getSfArgumentCompletions('start ') includes every registry template", () => {
		const completions = getSfArgumentCompletions("start ");
		const labels = completions.map((c: any) => c.label);
		const registry = loadRegistry();
		for (const id of Object.keys(registry.templates)) {
			assert.ok(labels.includes(id), `missing start completion: ${id}`);
		}
		assert.ok(labels.includes("resume"), "should include resume command");
		assert.ok(labels.includes("--dry-run"), "should include dry-run flag");
	});
});

// ─── /sf start Template Runs ─────────────────────────────────────────────

describe("workflow template start command", () => {
	async function callStart(trimmed: string, base: string) {
		process.chdir(base);
		const { handleWorkflowCommand } = await import(
			"../commands/handlers/workflow.ts"
		);
		const ctx = createMockCtx();
		const pi = createMockPi();
		const handled = await handleWorkflowCommand(trimmed, ctx as any, pi as any);
		return { handled, notifications: ctx.notifications, messages: pi.messages };
	}

	it("creates a graph-backed run for product-plan without sending a raw prompt", async () => {
		const base = makeTmpBase();
		initGitRepo(base);
		const { handled, notifications, messages } = await callStart(
			"start product-plan plan the product",
			base,
		);

		assert.ok(handled, "start command should be handled");
		assert.equal(
			messages.length,
			0,
			"normal template start should not send a raw prompt",
		);
		assert.ok(
			notifications.some((n) =>
				n.message.includes("Starting workflow: Product Plan"),
			),
			"should notify that product-plan started",
		);

		const runsRoot = join(base, ".sf", "workflow-runs", "product-plan");
		assert.ok(existsSync(runsRoot), "product-plan run root should exist");
		const [timestamp] = readdirSync(runsRoot);
		assert.ok(timestamp, "should create timestamped run dir");
		const runDir = join(runsRoot, timestamp);
		assert.ok(existsSync(join(runDir, "DEFINITION.yaml")));
		assert.ok(existsSync(join(runDir, "GRAPH.yaml")));
		assert.ok(existsSync(join(runDir, "RUN.json")));

		const runMeta = JSON.parse(readFileSync(join(runDir, "RUN.json"), "utf-8"));
		assert.equal(runMeta.kind, "template");
		assert.equal(runMeta.mode, "guided");
		assert.equal(runMeta.templateId, "product-plan");
	});

	it("dry-run product-plan creates no workflow run", async () => {
		const base = makeTmpBase();
		const { handled, notifications } = await callStart(
			"start --dry-run product-plan plan the product",
			base,
		);

		assert.ok(handled, "dry-run start command should be handled");
		assert.ok(
			notifications.some((n) => n.message.includes("DRY RUN")),
			"should report dry-run output",
		);
		assert.ok(
			!existsSync(join(base, ".sf", "workflow-runs", "product-plan")),
			"dry-run should not create a product-plan run",
		);
	});

	it("full-project keeps the existing prompt-dispatch special case", async () => {
		const base = makeTmpBase();
		const { handled, messages } = await callStart(
			"start full-project new app",
			base,
		);

		assert.ok(handled, "full-project start should be handled");
		assert.equal(
			messages.length,
			1,
			"full-project should still send a setup prompt",
		);
		assert.ok(
			!existsSync(join(base, ".sf", "workflow-runs", "full-project")),
			"full-project should not create a template run yet",
		);
	});
});

// ─── Command Handler Tests ───────────────────────────────────────────────

describe("workflow command handler", () => {
	// Dynamically import the handler so module-level side effects
	// don't break when auto.ts pulls in heavy runtime deps.
	// We test the pure routing logic by calling handleWorkflowCommand directly.

	async function callHandler(trimmed: string) {
		const { handleWorkflowCommand } = await import(
			"../commands/handlers/workflow.ts"
		);
		const ctx = createMockCtx();
		const pi = createMockPi();
		const handled = await handleWorkflowCommand(trimmed, ctx as any, pi as any);
		return { handled, notifications: ctx.notifications };
	}

	it("bare '/sf workflow' shows usage", async () => {
		const { handled, notifications } = await callHandler("workflow");
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some((n) => n.message.includes("Usage: /sf workflow")),
			"should show usage",
		);
	});

	it("'/sf workflow new' shows skill invocation message", async () => {
		const { handled, notifications } = await callHandler("workflow new");
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some((n) => n.message.includes("create-workflow")),
			"should mention create-workflow skill",
		);
	});

	it("'/sf workflow run' without name shows usage warning", async () => {
		const { handled, notifications } = await callHandler("workflow run");
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some(
				(n) => n.level === "warning" && n.message.includes("Usage"),
			),
			"should show usage warning",
		);
	});

	it("preserves quoted workflow run overrides (#4130)", async () => {
		const { parseWorkflowRunArgs } = await import(
			"../commands/handlers/workflow.ts"
		);
		assert.deepStrictEqual(
			parseWorkflowRunArgs(
				"demo-workflow target=\"multi word target\" region='us east'",
			),
			{
				defName: "demo-workflow",
				overrides: {
					target: "multi word target",
					region: "us east",
				},
			},
		);
	});

	it("'/sf workflow run nonexistent' shows error for missing definition", async () => {
		const { handled, notifications } = await callHandler(
			"workflow run nonexistent-def-12345",
		);
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some(
				(n) => n.level === "error" && n.message.includes("not found"),
			),
			"should show definition-not-found error",
		);
	});

	it("'/sf workflow validate' without name shows usage warning", async () => {
		const { handled, notifications } = await callHandler("workflow validate");
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some(
				(n) => n.level === "warning" && n.message.includes("Usage"),
			),
			"should show usage warning",
		);
	});

	it("'/sf workflow validate nonexistent' shows definition not found", async () => {
		const { handled, notifications } = await callHandler(
			"workflow validate nonexistent-def-12345",
		);
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some(
				(n) => n.level === "error" && n.message.includes("not found"),
			),
			"should show not-found error",
		);
	});

	it("'/sf workflow pause' without custom engine shows warning", async () => {
		const { handled, notifications } = await callHandler("workflow pause");
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some((n) => n.level === "warning"),
			"should show warning when no custom workflow is running",
		);
	});

	it("'/sf workflow resume' without custom engine shows warning", async () => {
		const { handled, notifications } = await callHandler("workflow resume");
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some((n) => n.level === "warning"),
			"should show warning when no custom workflow to resume",
		);
	});

	it("'/sf workflow unknown-sub' shows unknown subcommand", async () => {
		const { handled, notifications } = await callHandler("workflow blurble");
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some((n) =>
				n.message.includes("Unknown workflow subcommand"),
			),
			"should show unknown subcommand message",
		);
	});

	it("'/sf workflow list' with no runs shows empty message", async () => {
		const { handled, notifications } = await callHandler("workflow list");
		assert.ok(handled, "should be handled");
		assert.ok(
			notifications.some((n) => n.message.includes("No workflow runs found")),
			"should show no runs message",
		);
	});

	it("'/sf workflow list' shows template and definition source metadata", async () => {
		const base = makeTmpBase();
		writeDefinition(base, "deploy-pipeline", SIMPLE_DEF);
		process.chdir(base);

		const { createRun, createRunFromDefinition } = await import(
			"../run-manager.ts"
		);
		createRun(base, "deploy-pipeline");
		createRunFromDefinition(
			base,
			"product-plan",
			{
				version: 1,
				name: "product-plan",
				steps: [
					{
						id: "model",
						name: "model",
						prompt: "Model product",
						requires: [],
						produces: [],
					},
				],
			},
			{ kind: "template", mode: "guided", templateId: "product-plan" },
		);

		const { handled, notifications } = await callHandler("workflow list");
		assert.ok(handled, "workflow list should be handled");
		const output = notifications.map((n) => n.message).join("\n");
		assert.match(output, /deploy-pipeline.*\[definition\]/);
		assert.match(output, /product-plan.*\[template\]/);
	});

	it("non-workflow commands are not intercepted by custom workflow routing", async () => {
		const { handleWorkflowCommand } = await import(
			"../commands/handlers/workflow.ts"
		);
		const ctx = createMockCtx();
		const pi = createMockPi();
		// "queue" does not start with "workflow" so the custom routing should not handle it.
		// The function may still handle it via its existing dev-workflow routing, but it
		// should not be captured by the custom workflow `if` block.
		// We verify this by checking that a clearly non-workflow command like "somethingelse"
		// returns false (unhandled).
		const handled = await handleWorkflowCommand(
			"somethingelse",
			ctx as any,
			pi as any,
		);
		assert.equal(handled, false, "non-workflow commands should return false");
	});
});
