import assert from "node:assert/strict";
import {
	mkdirSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { test } from "vitest";

import { discoverAgents, validateAgentDefinition } from "../subagent/agents.js";
import {
	composeAgentPrompt,
	normalizePromptParts,
	validatePromptParts,
} from "../subagent/prompt-parts.js";

function makeProject() {
	const project = mkdtempSync(join(tmpdir(), "sf-agent-yaml-"));
	mkdirSync(join(project, ".sf", "agents"), { recursive: true });
	return project;
}

test("discoverAgents_when_project_has_copilot_style_agent_yaml_loads_agent", () => {
	const project = makeProject();
	writeFileSync(
		join(project, ".sf", "agents", "explore.agent.yaml"),
		[
			"name: explore",
			"displayName: Explore Agent",
			"description: >",
			"  Fast codebase exploration in a separate context window.",
			"model: claude-haiku-4.5",
			"tools:",
			"  - grep",
			"  - glob",
			"  - view",
			"promptParts:",
			"  - aiSafety",
			"  - toolInstructions",
			"prompt: |",
			"  You are an exploration agent.",
			"  Answer quickly and cite exact files.",
			"",
		].join("\n"),
	);

	const { agents, projectAgentsDir } = discoverAgents(project, "project");
	const agent = agents.find((entry) => entry.name === "explore");

	assert.equal(projectAgentsDir, join(project, ".sf", "agents"));
	assert.ok(agent);
	assert.equal(agent.displayName, "Explore Agent");
	assert.equal(
		agent.description,
		"Fast codebase exploration in a separate context window.\n",
	);
	assert.equal(agent.model, "claude-haiku-4.5");
	assert.deepEqual(agent.tools, ["grep", "glob", "view"]);
	assert.deepEqual(agent.promptParts, ["aiSafety", "toolInstructions"]);
	assert.match(agent.systemPrompt, /You are an exploration agent/);
});

test("discoverAgents_when_yaml_tools_wildcard_set_uses_default_full_tool_surface", () => {
	const project = makeProject();
	writeFileSync(
		join(project, ".sf", "agents", "task.agent.yaml"),
		[
			"name: task",
			"description: Execute commands and report results.",
			"tools:",
			'  - "*"',
			"prompt: |",
			"  Execute the requested command once.",
			"",
		].join("\n"),
	);

	const { agents } = discoverAgents(project, "project");
	const agent = agents.find((entry) => entry.name === "task");

	assert.ok(agent);
	assert.equal(agent.tools, undefined);
	assert.match(agent.systemPrompt, /Execute the requested command once/);
});

test("discoverAgents_when_yaml_and_markdown_share_name_prefers_later_project_entry", () => {
	const project = makeProject();
	writeFileSync(
		join(project, ".sf", "agents", "agent-a.md"),
		[
			"---",
			"name: duplicate",
			"description: Markdown definition",
			"---",
			"",
			"Markdown body.",
			"",
		].join("\n"),
	);
	writeFileSync(
		join(project, ".sf", "agents", "agent-z.agent.yaml"),
		[
			"name: duplicate",
			"description: YAML definition",
			"prompt: |",
			"  YAML body.",
			"",
		].join("\n"),
	);

	const { agents } = discoverAgents(project, "project");
	const duplicates = agents.filter((entry) => entry.name === "duplicate");

	assert.equal(duplicates.length, 1);
	assert.equal(duplicates[0].description, "YAML definition");
	assert.match(duplicates[0].systemPrompt, /YAML body/);
});

test("discoverAgents_when_scope_both_includes_builtin_review_code_and_triage_decider", () => {
	// Built-in agents ship with SF (src/resources/extensions/sf/agents/) and
	// must be discoverable without operator setup. They're the foundation for
	// SF's self-driven triage pipeline (sf-mp5lnlbc-ty5fec). Isolate from the
	// real ~/.sf/agent/agents/ so the test doesn't conflict with the
	// operator's personal agents if present.
	const isolatedAgentDir = mkdtempSync(join(tmpdir(), "sf-agent-dir-"));
	const originalEnv = process.env.SF_CODING_AGENT_DIR;
	process.env.SF_CODING_AGENT_DIR = isolatedAgentDir;
	try {
		const project = makeProject();
		const { agents } = discoverAgents(project, "both");
		const reviewCode = agents.find((a) => a.name === "review-code");
		const triageDecider = agents.find((a) => a.name === "triage-decider");
		assert.ok(
			reviewCode,
			"review-code builtin agent must be discovered in scope=both",
		);
		assert.equal(reviewCode.source, "builtin");
		assert.match(reviewCode.description, /constructive critic/i);
		assert.ok(
			triageDecider,
			"triage-decider builtin agent must be discovered in scope=both",
		);
		assert.equal(triageDecider.source, "builtin");
		// triage-decider is plan-only; the apply runner owns mutation after
		// review-code agrees.
		assert.ok(
			!triageDecider.tools?.includes("resolve_issue"),
			"triage-decider must not declare resolve_issue tool access",
		);
	} finally {
		if (originalEnv === undefined) delete process.env.SF_CODING_AGENT_DIR;
		else process.env.SF_CODING_AGENT_DIR = originalEnv;
	}
});

test("discoverAgents_when_project_overrides_builtin_name_project_wins", () => {
	// Project-level definitions must shadow built-ins with the same name —
	// operators need to be able to customize review-code/triage-decider
	// behavior per project without forking the SF distribution.
	const isolatedAgentDir = mkdtempSync(join(tmpdir(), "sf-agent-dir-"));
	const originalEnv = process.env.SF_CODING_AGENT_DIR;
	process.env.SF_CODING_AGENT_DIR = isolatedAgentDir;
	try {
		const project = makeProject();
		writeFileSync(
			join(project, ".sf", "agents", "review-code.agent.yaml"),
			[
				"name: review-code",
				"description: Project-level override of review-code.",
				"prompt: |",
				"  This is the project override.",
				"",
			].join("\n"),
		);
		const { agents } = discoverAgents(project, "both");
		const matches = agents.filter((a) => a.name === "review-code");
		assert.equal(matches.length, 1);
		assert.equal(matches[0].source, "project");
		assert.match(matches[0].description, /Project-level override/);
	} finally {
		if (originalEnv === undefined) delete process.env.SF_CODING_AGENT_DIR;
		else process.env.SF_CODING_AGENT_DIR = originalEnv;
	}
});

test("validateAgentDefinition_when_builtin_promptParts_unknown_name_reports_error", () => {
	const errors = validateAgentDefinition({
		name: "bad-agent",
		description: "Bad agent",
		systemPrompt: "Do the thing.",
		promptParts: ["aiSafety", "includeInventedFuturePart"],
	});

	assert.ok(
		errors.some((error) =>
			error.includes('"includeInventedFuturePart" is not a known prompt part'),
		),
	);
});

test("validatePromptParts_when_legacy_boolean_shape_is_used_rejects_it", () => {
	assert.deepEqual(validatePromptParts({ includeAISafety: true }), [
		"promptParts must be an array of canonical part names",
	]);
});

test("composeAgentPrompt_when_prompt_parts_absent_preserves_raw_prompt", () => {
	assert.equal(
		composeAgentPrompt({
			name: "plain",
			description: "plain",
			systemPrompt: "Plain prompt.",
		}),
		"Plain prompt.",
	);
});

test("composeAgentPrompt_when_prompt_parts_present_uses_registry_order", () => {
	const prompt = composeAgentPrompt(
		{
			name: "explore",
			description: "Explore.",
			systemPrompt: "Agent body.",
			promptParts: ["toolInstructions", "aiSafety"],
			tools: ["grep", "view"],
		},
		{ tools: ["grep", "view"] },
	);

	assert.match(prompt, /## AI Safety/);
	assert.match(prompt, /## Tool Instructions/);
	assert.match(prompt, /## Agent Instructions/);
	assert.ok(
		prompt.indexOf("## AI Safety") < prompt.indexOf("## Tool Instructions"),
	);
	assert.ok(
		prompt.indexOf("## Tool Instructions") <
			prompt.indexOf("## Agent Instructions"),
	);
	assert.match(prompt, /Available tools for this agent: grep, view/);
	assert.match(prompt, /Agent body\./);
});

test("composeAgentPrompt_when_output_contract_declared_appends_contract_section", () => {
	const prompt = composeAgentPrompt(
		{
			name: "contracted",
			description: "Contracted.",
			systemPrompt: "Agent body.",
			promptParts: ["outputContract"],
		},
		{ outputContract: "Return JSON only." },
	);

	assert.match(prompt, /## Agent Instructions/);
	assert.match(prompt, /Agent body\./);
	assert.match(prompt, /## Output Contract/);
	assert.match(prompt, /Return JSON only\./);
	assert.ok(
		prompt.indexOf("## Agent Instructions") <
			prompt.indexOf("## Output Contract"),
	);
});

test("normalizePromptParts_when_given_canonical_names_always_includes_agent_body", () => {
	assert.deepEqual(normalizePromptParts(["toolInstructions", "aiSafety"]), [
		"aiSafety",
		"toolInstructions",
		"agentBody",
	]);
});

test("discoverAgents_when_scope_both_validates_builtin_promptParts_contract", () => {
	const isolatedAgentDir = mkdtempSync(join(tmpdir(), "sf-agent-dir-"));
	const originalEnv = process.env.SF_CODING_AGENT_DIR;
	process.env.SF_CODING_AGENT_DIR = isolatedAgentDir;
	try {
		const project = makeProject();
		const { agents } = discoverAgents(project, "both");
		for (const agent of agents.filter((entry) => entry.source === "builtin")) {
			assert.deepEqual(
				validateAgentDefinition(agent),
				[],
				`${agent.name} built-in agent should validate`,
			);
		}
	} finally {
		if (originalEnv === undefined) delete process.env.SF_CODING_AGENT_DIR;
		else process.env.SF_CODING_AGENT_DIR = originalEnv;
	}
});

test("discoverAgents_when_loading_bundled_agents_marks_them_internal_by_default_policy", () => {
	const isolatedAgentDir = mkdtempSync(join(tmpdir(), "sf-agent-dir-"));
	const originalEnv = process.env.SF_CODING_AGENT_DIR;
	process.env.SF_CODING_AGENT_DIR = isolatedAgentDir;
	try {
		const project = makeProject();
		const { agents } = discoverAgents(project, "both");
		const bundled = agents.filter((entry) => entry.source === "builtin");

		assert.ok(bundled.length > 0, "expected bundled SF agents");
		for (const agent of bundled) {
			assert.equal(
				agent.visibility,
				"internal",
				`${agent.name} should be internal; SF uses bundled agents as an autonomous worker pool, not a user-facing CLI-coder palette`,
			);
		}
	} finally {
		if (originalEnv === undefined) delete process.env.SF_CODING_AGENT_DIR;
		else process.env.SF_CODING_AGENT_DIR = originalEnv;
	}
});

test("bundled_markdown_agents_are_internal_autonomous_workers_not_public_palette", () => {
	const agentsDir = resolve(import.meta.dirname, "..", "..", "..", "agents");
	const files = readdirSync(agentsDir).filter((file) => file.endsWith(".md"));

	assert.ok(files.length > 0, "expected bundled markdown agents");
	for (const file of files) {
		const content = readFileSync(join(agentsDir, file), "utf8");
		assert.match(
			content,
			/^visibility:\s*internal$/m,
			`${file} should be internal; SF is autonomous-first, not a CLI-coder agent marketplace`,
		);
	}
});
