/**
 * Tests for S02 CLI surface — --output-format, exit codes, HeadlessJsonResult, --resume.
 *
 * Uses extracted parsing logic (mirrors headless.ts) and direct imports from
 * headless-types.ts / headless-events.ts to avoid transitive @singularity-forge/native
 * import that breaks in test environment.
 */

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

// ─── Import exit code constants & mapStatusToExitCode ──────────────────────

import {
	classifyUnexpectedChildExit,
	EXIT_BLOCKED,
	EXIT_CANCELLED,
	EXIT_ERROR,
	EXIT_SUCCESS,
	mapStatusToExitCode,
	shouldRestartHeadlessRun,
} from "../headless-events.js";

import type { HeadlessJsonResult, OutputFormat } from "../headless-types.js";
import { VALID_OUTPUT_FORMATS } from "../headless-types.js";
import {
	findUnsupportedAutonomousArgs,
	formatUnsupportedAutonomousArgs,
} from "../resources/extensions/sf/autonomous-command-args.js";

// ─── Extracted parsing logic (mirrors headless.ts) ─────────────────────────

interface HeadlessOptions {
	timeout: number;
	json: boolean;
	outputFormat: OutputFormat;
	model?: string;
	command: string;
	commandArgs: string[];
	context?: string;
	contextText?: string;
	chainAutonomous?: boolean;
	verbose?: boolean;
	maxRestarts?: number;
	supervised?: boolean;
	responseTimeout?: number;
	answers?: string;
	eventFilter?: Set<string>;
	resumeSession?: string;
	bare?: boolean;
	skipPddCheck?: boolean;
}

function parseHeadlessArgs(argv: string[]): HeadlessOptions {
	const options: HeadlessOptions = {
		timeout: 300_000,
		json: false,
		outputFormat: "text",
		command: "help",
		commandArgs: [],
	};

	const args = argv.slice(2);
	let commandSeen = false;

	for (let i = 0; i < args.length; i++) {
		const arg = args[i];
		if (arg === "headless") continue;

		if (arg.startsWith("--")) {
			if (arg === "--timeout" && i + 1 < args.length) {
				options.timeout = parseInt(args[++i], 10);
			} else if (arg === "--json") {
				options.json = true;
				options.outputFormat = "stream-json";
			} else if (arg === "--output-format" && i + 1 < args.length) {
				const fmt = args[++i];
				if (!VALID_OUTPUT_FORMATS.has(fmt)) {
					throw new Error(`Invalid output format: ${fmt}`);
				}
				options.outputFormat = fmt as OutputFormat;
				if (fmt === "stream-json" || fmt === "json") {
					options.json = true;
				}
			} else if (arg === "--model" && i + 1 < args.length) {
				options.model = args[++i];
			} else if (arg === "--context" && i + 1 < args.length) {
				options.context = args[++i];
			} else if (arg === "--context-text" && i + 1 < args.length) {
				options.contextText = args[++i];
			} else if (arg === "--autonomous") {
				options.chainAutonomous = true;
			} else if (arg === "--auto") {
				throw new Error(
					"--auto was removed. Use --autonomous to chain into autonomous mode.",
				);
			} else if (arg === "--verbose") {
				options.verbose = true;
			} else if (arg === "--max-restarts" && i + 1 < args.length) {
				options.maxRestarts = parseInt(args[++i], 10);
			} else if (arg === "--answers" && i + 1 < args.length) {
				options.answers = args[++i];
			} else if (arg === "--events" && i + 1 < args.length) {
				options.eventFilter = new Set(args[++i].split(","));
				options.json = true;
				if (options.outputFormat === "text") {
					options.outputFormat = "stream-json";
				}
			} else if (arg === "--supervised") {
				options.supervised = true;
				options.json = true;
				if (options.outputFormat === "text") {
					options.outputFormat = "stream-json";
				}
			} else if (arg === "--response-timeout" && i + 1 < args.length) {
				options.responseTimeout = parseInt(args[++i], 10);
			} else if (arg === "--resume" && i + 1 < args.length) {
				options.resumeSession = args[++i];
			} else if (arg === "--bare") {
				options.bare = true;
			} else if (arg === "--skip-pdd-check") {
				options.skipPddCheck = true;
			} else if (commandSeen) {
				options.commandArgs.push(arg);
			}
		} else if (!commandSeen) {
			if (arg === "autonomous" || arg === "auto") {
				options.command = "autonomous";
				options.chainAutonomous = true;
			} else {
				options.command = arg;
			}
			commandSeen = true;
		} else {
			options.commandArgs.push(arg);
		}
	}

	return options;
}

// ─── --output-format flag parsing ──────────────────────────────────────────

test("bare headless defaults to help, not autonomous", () => {
	const opts = parseHeadlessArgs(["node", "sf", "headless"]);
	assert.equal(opts.command, "help");
	assert.equal(opts.chainAutonomous, undefined);
	assert.deepEqual(opts.commandArgs, []);
});

test("--output-format text sets outputFormat to text", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--output-format",
		"text",
		"autonomous",
	]);
	assert.equal(opts.outputFormat, "text");
	assert.equal(opts.json, false);
});

test("--output-format json sets outputFormat to json and json=true", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--output-format",
		"json",
		"autonomous",
	]);
	assert.equal(opts.outputFormat, "json");
	assert.equal(opts.json, true);
});

test("--output-format stream-json sets outputFormat to stream-json and json=true", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--output-format",
		"stream-json",
		"autonomous",
	]);
	assert.equal(opts.outputFormat, "stream-json");
	assert.equal(opts.json, true);
});

test("default output format is text", () => {
	const opts = parseHeadlessArgs(["node", "sf", "headless", "autonomous"]);
	assert.equal(opts.outputFormat, "text");
	assert.equal(opts.json, false);
});

test("autonomous command is accepted as headless command", () => {
	const opts = parseHeadlessArgs(["node", "sf", "headless", "autonomous"]);
	assert.equal(opts.command, "autonomous");
	assert.deepEqual(opts.commandArgs, []);
});

test("auto alias is normalized to autonomous headless command", () => {
	const opts = parseHeadlessArgs(["node", "sf", "headless", "auto"]);
	assert.equal(opts.command, "autonomous");
	assert.equal(opts.chainAutonomous, true);
	assert.deepEqual(opts.commandArgs, []);
});

test("autonomous command preserves command arguments", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"autonomous",
		"M001",
		"extra-context",
	]);
	assert.equal(opts.command, "autonomous");
	assert.deepEqual(opts.commandArgs, ["M001", "extra-context"]);
});

test("invalid --output-format value throws", () => {
	assert.throws(
		() =>
			parseHeadlessArgs([
				"node",
				"sf",
				"headless",
				"--output-format",
				"yaml",
				"autonomous",
			]),
		/Invalid output format: yaml/,
	);
});

test("invalid --output-format value (empty) throws", () => {
	assert.throws(
		() =>
			parseHeadlessArgs([
				"node",
				"sf",
				"headless",
				"--output-format",
				"xml",
				"autonomous",
			]),
		/Invalid output format/,
	);
});

// ─── --json backward compatibility ─────────────────────────────────────────

test("--json is alias for --output-format stream-json", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--json",
		"autonomous",
	]);
	assert.equal(opts.outputFormat, "stream-json");
	assert.equal(opts.json, true);
});

test("--json before --output-format json: last writer wins", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--json",
		"--output-format",
		"json",
		"autonomous",
	]);
	assert.equal(opts.outputFormat, "json");
	assert.equal(opts.json, true);
});

// ─── --resume flag ─────────────────────────────────────────────────────────

test("--resume parses session ID", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--resume",
		"abc-123",
		"autonomous",
	]);
	assert.equal(opts.resumeSession, "abc-123");
	assert.equal(opts.command, "autonomous");
});

test("no --resume means undefined", () => {
	const opts = parseHeadlessArgs(["node", "sf", "headless", "autonomous"]);
	assert.equal(opts.resumeSession, undefined);
});

test("unknown command flags after subcommand are preserved for slash command handlers", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"autonomous",
		"--turbo",
	]);

	assert.equal(opts.command, "autonomous");
	assert.deepEqual(opts.commandArgs, ["--turbo"]);
});

test("unsupported autonomous args reject invented knobs and report supported grammar", () => {
	assert.deepEqual(findUnsupportedAutonomousArgs(["--turbo"]), ["--turbo"]);
	assert.deepEqual(findUnsupportedAutonomousArgs(["turbo"]), ["turbo"]);
	assert.deepEqual(findUnsupportedAutonomousArgs(["--yolo", "brief.md"]), []);
	assert.deepEqual(findUnsupportedAutonomousArgs(["M001", "--debug"]), []);
	const message = formatUnsupportedAutonomousArgs(["--turbo"]);
	assert.match(message, /Unsupported/);
	assert.match(message, /--yolo <file>/);
});

// ─── Exit code constants ───────────────────────────────────────────────────

test("EXIT_SUCCESS is 0", () => {
	assert.equal(EXIT_SUCCESS, 0);
});

test("EXIT_ERROR is 1", () => {
	assert.equal(EXIT_ERROR, 1);
});

test("EXIT_BLOCKED is 10", () => {
	assert.equal(EXIT_BLOCKED, 10);
});

test("EXIT_CANCELLED is 11", () => {
	assert.equal(EXIT_CANCELLED, 11);
});

// ─── mapStatusToExitCode ───────────────────────────────────────────────────

test("mapStatusToExitCode: success → 0", () => {
	assert.equal(mapStatusToExitCode("success"), EXIT_SUCCESS);
});

test("mapStatusToExitCode: complete → 0", () => {
	assert.equal(mapStatusToExitCode("complete"), EXIT_SUCCESS);
});

test("mapStatusToExitCode: error → 1", () => {
	assert.equal(mapStatusToExitCode("error"), EXIT_ERROR);
});

test("mapStatusToExitCode: timeout → 1", () => {
	assert.equal(mapStatusToExitCode("timeout"), EXIT_ERROR);
});

test("mapStatusToExitCode: blocked → 10", () => {
	assert.equal(mapStatusToExitCode("blocked"), EXIT_BLOCKED);
});

test("mapStatusToExitCode: cancelled → 11", () => {
	assert.equal(mapStatusToExitCode("cancelled"), EXIT_CANCELLED);
});

test("mapStatusToExitCode: unknown status defaults to EXIT_ERROR", () => {
	assert.equal(mapStatusToExitCode("unknown"), EXIT_ERROR);
	assert.equal(mapStatusToExitCode(""), EXIT_ERROR);
});

test("shouldRestartHeadlessRun returns false for operator-bounded timeout", () => {
	assert.equal(
		shouldRestartHeadlessRun({
			exitCode: EXIT_ERROR,
			timedOut: true,
			interrupted: false,
			restartCount: 0,
			maxRestarts: 3,
		}),
		false,
	);
});

test("shouldRestartHeadlessRun still retries unexpected errors within budget", () => {
	assert.equal(
		shouldRestartHeadlessRun({
			exitCode: EXIT_ERROR,
			timedOut: false,
			interrupted: false,
			restartCount: 0,
			maxRestarts: 3,
		}),
		true,
	);
});

test("classifyUnexpectedChildExit treats graceful child exit as terminal success", () => {
	assert.equal(classifyUnexpectedChildExit(0, null), EXIT_SUCCESS);
});

test("classifyUnexpectedChildExit treats operator child signal as cancellation", () => {
	assert.equal(classifyUnexpectedChildExit(null, "SIGTERM"), EXIT_CANCELLED);
	assert.equal(classifyUnexpectedChildExit(null, "SIGINT"), EXIT_CANCELLED);
});

test("classifyUnexpectedChildExit keeps nonzero child exits restartable errors", () => {
	assert.equal(classifyUnexpectedChildExit(1, null), EXIT_ERROR);
});

// ─── HeadlessJsonResult type shape ─────────────────────────────────────────

test("HeadlessJsonResult satisfies expected shape", () => {
	// Type-level assertion: construct a valid object and verify it compiles.
	// At runtime, verify all required keys exist.
	const result: HeadlessJsonResult = {
		schemaVersion: 1,
		status: "success",
		exitCode: 0,
		duration: 12345,
		cost: {
			total: 0.05,
			input_tokens: 1000,
			output_tokens: 500,
			cache_read_tokens: 200,
			cache_write_tokens: 100,
		},
		toolCalls: 15,
		events: 42,
	};
	assert.equal(result.status, "success");
	assert.equal(result.schemaVersion, 1);
	assert.equal(result.exitCode, 0);
	assert.equal(typeof result.duration, "number");
	assert.ok(result.cost);
	assert.equal(typeof result.cost.total, "number");
	assert.equal(typeof result.cost.input_tokens, "number");
	assert.equal(typeof result.cost.output_tokens, "number");
	assert.equal(typeof result.cost.cache_read_tokens, "number");
	assert.equal(typeof result.cost.cache_write_tokens, "number");
	assert.equal(typeof result.toolCalls, "number");
	assert.equal(typeof result.events, "number");
});

test("HeadlessJsonResult accepts optional fields", () => {
	const result: HeadlessJsonResult = {
		schemaVersion: 1,
		status: "blocked",
		exitCode: 10,
		sessionId: "sess-abc",
		duration: 5000,
		cost: {
			total: 0,
			input_tokens: 0,
			output_tokens: 0,
			cache_read_tokens: 0,
			cache_write_tokens: 0,
		},
		toolCalls: 0,
		events: 1,
		milestone: "M001",
		phase: "planning",
		nextAction: "fix blocker",
		artifacts: ["ROADMAP.md"],
		commits: ["abc1234"],
	};
	assert.equal(result.sessionId, "sess-abc");
	assert.equal(result.milestone, "M001");
	assert.deepEqual(result.artifacts, ["ROADMAP.md"]);
	assert.deepEqual(result.commits, ["abc1234"]);
});

// ─── VALID_OUTPUT_FORMATS set ──────────────────────────────────────────────

test("VALID_OUTPUT_FORMATS contains exactly text, json, stream-json", () => {
	assert.equal(VALID_OUTPUT_FORMATS.size, 3);
	assert.ok(VALID_OUTPUT_FORMATS.has("text"));
	assert.ok(VALID_OUTPUT_FORMATS.has("json"));
	assert.ok(VALID_OUTPUT_FORMATS.has("stream-json"));
});

// ─── Regression: existing flags still parse correctly ──────────────────────

test("--events still works with new outputFormat default", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--events",
		"agent_end,tool_execution_start",
		"autonomous",
	]);
	assert.ok(opts.eventFilter instanceof Set);
	assert.equal(opts.eventFilter!.size, 2);
	assert.equal(opts.json, true);
	assert.equal(opts.outputFormat, "stream-json");
});

test("--timeout still works", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--timeout",
		"60000",
		"autonomous",
	]);
	assert.equal(opts.timeout, 60000);
});

test("--supervised still works and implies stream-json", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--supervised",
		"autonomous",
	]);
	assert.equal(opts.supervised, true);
	assert.equal(opts.json, true);
	assert.equal(opts.outputFormat, "stream-json");
});

test("--answers still works", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--answers",
		"answers.json",
		"autonomous",
	]);
	assert.equal(opts.answers, "answers.json");
});

test("positional command parsing still works", () => {
	const opts = parseHeadlessArgs(["node", "sf", "headless", "next"]);
	assert.equal(opts.command, "next");
});

test("combined flags parse correctly", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--output-format",
		"json",
		"--timeout",
		"120000",
		"--resume",
		"sess-xyz",
		"--verbose",
		"autonomous",
	]);
	assert.equal(opts.outputFormat, "json");
	assert.equal(opts.json, true);
	assert.equal(opts.timeout, 120000);
	assert.equal(opts.resumeSession, "sess-xyz");
	assert.equal(opts.verbose, true);
	assert.equal(opts.command, "autonomous");
});

// ─── --bare flag ───────────────────────────────────────────────────────────

test("--bare sets bare to true", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--bare",
		"autonomous",
	]);
	assert.equal(opts.bare, true);
	assert.equal(opts.command, "autonomous");
});

test("no --bare means bare is undefined", () => {
	const opts = parseHeadlessArgs(["node", "sf", "headless", "autonomous"]);
	assert.equal(opts.bare, undefined);
});

test("--bare is a boolean flag (no value needed)", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--bare",
		"--json",
		"autonomous",
	]);
	assert.equal(opts.bare, true);
	assert.equal(opts.json, true);
});

test("--bare combined with --output-format json", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--bare",
		"--output-format",
		"json",
		"autonomous",
	]);
	assert.equal(opts.bare, true);
	assert.equal(opts.outputFormat, "json");
	assert.equal(opts.json, true);
	assert.equal(opts.command, "autonomous");
});

// ─── Command-first ordering (flags after command) ─────────────────────────

test("command before flags: new-milestone --context-text --autonomous --verbose", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"new-milestone",
		"--context-text",
		"build something cool",
		"--autonomous",
		"--verbose",
	]);
	assert.equal(opts.command, "new-milestone");
	assert.equal(opts.contextText, "build something cool");
	assert.equal(opts.chainAutonomous, true);
	assert.equal(opts.verbose, true);
});

test("command before flags: next --json --timeout", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"next",
		"--json",
		"--timeout",
		"60000",
	]);
	assert.equal(opts.command, "next");
	assert.equal(opts.json, true);
	assert.equal(opts.timeout, 60000);
});

test("command between flags: --autonomous new-milestone --verbose", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--autonomous",
		"new-milestone",
		"--verbose",
	]);
	assert.equal(opts.command, "new-milestone");
	assert.equal(opts.chainAutonomous, true);
	assert.equal(opts.verbose, true);
});

test("removed --auto shorthand is rejected", () => {
	assert.throws(
		() =>
			parseHeadlessArgs(["node", "sf", "headless", "new-milestone", "--auto"]),
		/--auto was removed/,
	);
});

test("--bare does not affect other flags", () => {
	const opts = parseHeadlessArgs([
		"node",
		"sf",
		"headless",
		"--bare",
		"--timeout",
		"60000",
		"--resume",
		"sess-abc",
		"autonomous",
	]);
	assert.equal(opts.bare, true);
	assert.equal(opts.timeout, 60000);
	assert.equal(opts.resumeSession, "sess-abc");
	assert.equal(opts.command, "autonomous");
});
