/**
 * Skills system tests.
 *
 * Purpose: verify skill discovery, frontmatter parsing, validation, and loading.
 */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { getSkillSearchDirs } from "../preferences-skills.js";
import { discoverSkillDirs, readSkillFile } from "../skills/directory.js";
import {
	buildSkillRecord,
	parseSkillFrontmatter,
	validateSkillFrontmatter,
} from "../skills/frontmatter.js";
import {
	getModelInvocableSkills,
	getPermittedSkills,
	getUserInvocableSkills,
	loadSkills,
} from "../skills/loader.js";

describe("skill frontmatter", () => {
	test("parseSkillFrontmatter_extracts_yaml_and_body", () => {
		const content = `---
name: test-skill
description: A test skill
user-invocable: true
model-invocable: false
side-effects: none
permission-profile: restricted
---

# Test Skill

Some instructions.
`;
		const parsed = parseSkillFrontmatter(content);
		expect(parsed).toBeTruthy();
		expect(parsed.frontmatter.name).toBe("test-skill");
		expect(parsed.frontmatter.description).toBe("A test skill");
		expect(parsed.frontmatter["user-invocable"]).toBe(true);
		expect(parsed.frontmatter["model-invocable"]).toBe(false);
		expect(parsed.frontmatter["side-effects"]).toBe("none");
		expect(parsed.frontmatter["permission-profile"]).toBe("restricted");
		expect(parsed.body).toContain("# Test Skill");
	});

	test("parseSkillFrontmatter_returns_null_without_frontmatter", () => {
		const parsed = parseSkillFrontmatter("# Just markdown\n\nNo frontmatter.");
		expect(parsed).toBeNull();
	});

	test("validateSkillFrontmatter_passes_complete_frontmatter", () => {
		const result = validateSkillFrontmatter({
			name: "test",
			description: "desc",
			"user-invocable": true,
			"model-invocable": false,
			"side-effects": "none",
			"permission-profile": "normal",
		});
		expect(result.valid).toBe(true);
		expect(result.errors).toHaveLength(0);
	});

	test("validateSkillFrontmatter_passes_minimal_ecosystem_frontmatter", () => {
		// Ecosystem standard: only name + description required
		const result = validateSkillFrontmatter({
			name: "test",
			description: "desc",
		});
		expect(result.valid).toBe(true);
		expect(result.errors).toHaveLength(0);
	});

	test("validateSkillFrontmatter_fails_missing_fields", () => {
		const result = validateSkillFrontmatter({
			name: "test",
		});
		expect(result.valid).toBe(false);
		expect(result.errors.length).toBeGreaterThan(0);
		expect(result.errors.some((e) => e.includes("description"))).toBe(true);
	});

	test("validateSkillFrontmatter_fails_invalid_permission_profile", () => {
		const result = validateSkillFrontmatter({
			name: "test",
			description: "desc",
			"user-invocable": true,
			"model-invocable": false,
			"side-effects": "none",
			"permission-profile": "invalid",
		});
		expect(result.valid).toBe(false);
		expect(result.errors.some((e) => e.includes("permission-profile"))).toBe(
			true,
		);
	});

	test("buildSkillRecord_creates_canonical_record", () => {
		const record = buildSkillRecord(
			"/path/to/skill",
			{
				name: "test-skill",
				description: "A test",
				"user-invocable": true,
				"model-invocable": true,
				"side-effects": "code-edits",
				"permission-profile": "normal",
				triggers: ["build", "code"],
				"max-activations": 5,
			},
			"# Body",
		);
		expect(record.name).toBe("test-skill");
		expect(record.userInvocable).toBe(true);
		expect(record.modelInvocable).toBe(true);
		expect(record.sideEffects).toBe("code-edits");
		expect(record.permissionProfile).toBe("normal");
		expect(record.triggers).toEqual(["build", "code"]);
		expect(record.maxActivations).toBe(5);
		expect(record.body).toBe("# Body");
		expect(record.path).toBe("/path/to/skill");
	});
});

describe("skill discovery", () => {
	let tmpDir;

	beforeEach(() => {
		tmpDir = mkdtempSync(join(tmpdir(), "sf-skills-test-"));
	});

	afterEach(() => {
		rmSync(tmpDir, { recursive: true, force: true });
	});

	test("discoverSkillDirs_finds_skills_with_skill_md", () => {
		const skillDir = join(tmpDir, ".agents", "skills", "test-skill");
		mkdirSync(skillDir, { recursive: true });
		writeFileSync(
			join(skillDir, "SKILL.md"),
			`---\nname: test-skill\ndescription: Test\nuser-invocable: true\nmodel-invocable: true\nside-effects: none\npermission-profile: restricted\n---\n\n# Test\n`,
		);

		const dirs = discoverSkillDirs(tmpDir);
		expect(dirs).toHaveLength(1);
		expect(dirs[0].name).toBe("test-skill");
	});

	test("discoverSkillDirs_ignores_dirs_without_skill_md", () => {
		const skillDir = join(tmpDir, ".agents", "skills", "empty-dir");
		mkdirSync(skillDir, { recursive: true });

		const dirs = discoverSkillDirs(tmpDir);
		expect(dirs).toHaveLength(0);
	});

	test("readSkillFile_reads_content", () => {
		const skillDir = join(tmpDir, ".agents", "skills", "test-skill");
		mkdirSync(skillDir, { recursive: true });
		writeFileSync(join(skillDir, "SKILL.md"), "# Test Skill\n");

		const content = readSkillFile(skillDir);
		expect(content).toBe("# Test Skill\n");
	});

	test("readSkillFile_returns_null_for_missing", () => {
		const content = readSkillFile(join(tmpDir, "nonexistent"));
		expect(content).toBeNull();
	});
});

describe("skill loading", () => {
	let tmpDir;

	beforeEach(() => {
		tmpDir = mkdtempSync(join(tmpdir(), "sf-skills-test-"));
	});

	afterEach(() => {
		rmSync(tmpDir, { recursive: true, force: true });
	});

	function createSkill(name, overrides = {}) {
		const skillDir = join(tmpDir, ".agents", "skills", name);
		mkdirSync(skillDir, { recursive: true });
		const frontmatter = {
			name,
			description: overrides.description ?? `Description for ${name}`,
			"user-invocable": overrides.userInvocable ?? true,
			"model-invocable": overrides.modelInvocable ?? true,
			"side-effects": overrides.sideEffects ?? "none",
			"permission-profile": overrides.permissionProfile ?? "normal",
			triggers: overrides.triggers ?? ["*"],
			...overrides.extra,
		};
		const yaml = Object.entries(frontmatter)
			.map(([k, v]) => {
				if (Array.isArray(v))
					return `${k}:\n${v.map((i) => `  - ${i}`).join("\n")}`;
				return `${k}: ${v}`;
			})
			.join("\n");
		writeFileSync(
			join(skillDir, "SKILL.md"),
			`---\n${yaml}\n---\n\n# ${name}\n`,
		);
	}

	test("loadSkills_loads_valid_skills", () => {
		createSkill("skill-a");
		createSkill("skill-b", { permissionProfile: "trusted" });

		const skills = loadSkills(tmpDir, { includeWorkflow: false });
		expect(skills).toHaveLength(2);
		expect(skills.every((s) => s.valid)).toBe(true);
		expect(skills.some((s) => s.name === "skill-a")).toBe(true);
		expect(skills.some((s) => s.name === "skill-b")).toBe(true);
	});

	test("loadSkills_project_skill_uses_repo_overlay_without_hidden_locking", () => {
		createSkill("sf-repo-orientation");

		const skills = loadSkills(tmpDir, {
			includeWorkflow: true,
			includeBundled: true,
		});
		const repoMap = skills.filter((s) => s.name === "sf-repo-orientation");

		expect(repoMap).toHaveLength(1);
		expect(repoMap[0].source).toBe("project");
	});

	test("loadSkills_marks_invalid_skills", () => {
		createSkill("valid-skill");
		const badDir = join(tmpDir, ".agents", "skills", "bad-skill");
		mkdirSync(badDir, { recursive: true });
		writeFileSync(join(badDir, "SKILL.md"), "No frontmatter here.");

		const skills = loadSkills(tmpDir, { includeWorkflow: false });
		expect(skills).toHaveLength(2);
		const bad = skills.find((s) => s.name === "bad-skill");
		expect(bad).toBeTruthy();
		expect(bad.valid).toBe(false);
	});

	test("getPermittedSkills_filters_by_profile", () => {
		createSkill("restricted-skill", { permissionProfile: "restricted" });
		createSkill("normal-skill", { permissionProfile: "normal" });
		createSkill("trusted-skill", { permissionProfile: "trusted" });

		const skills = loadSkills(tmpDir, { includeWorkflow: false });
		const permitted = getPermittedSkills(skills, "normal");
		expect(permitted).toHaveLength(2);
		expect(permitted.some((s) => s.name === "restricted-skill")).toBe(true);
		expect(permitted.some((s) => s.name === "normal-skill")).toBe(true);
		expect(permitted.some((s) => s.name === "trusted-skill")).toBe(false);
	});

	test("getModelInvocableSkills_filters_by_work_mode", () => {
		createSkill("build-skill", { triggers: ["build"], modelInvocable: true });
		createSkill("review-skill", { triggers: ["review"], modelInvocable: true });
		createSkill("universal-skill", { triggers: ["*"], modelInvocable: true });
		createSkill("user-only", { triggers: ["*"], modelInvocable: false });

		const skills = loadSkills(tmpDir, { includeWorkflow: false });
		const buildSkills = getModelInvocableSkills(skills, "build");
		expect(buildSkills).toHaveLength(2);
		expect(buildSkills.some((s) => s.name === "build-skill")).toBe(true);
		expect(buildSkills.some((s) => s.name === "universal-skill")).toBe(true);
		expect(buildSkills.some((s) => s.name === "review-skill")).toBe(false);
		expect(buildSkills.some((s) => s.name === "user-only")).toBe(false);
	});

	test("getUserInvocableSkills_excludes_locked_and_non_canonical_bundled", () => {
		createSkill("human-facing", { userInvocable: true });
		createSkill("autoresearch", { userInvocable: false });
		const badDir = join(tmpDir, ".agents", "skills", "droid-evolved");
		mkdirSync(badDir, { recursive: true });
		writeFileSync(
			join(badDir, "SKILL.md"),
			`---\nname: droid-evolved\ndescription: Workflow-only skill\nuser-invocable: true\n---\n\n# Invalid\n`,
		);

		const visible = getUserInvocableSkills([
			...loadSkills(tmpDir, { includeWorkflow: false }),
			{
				name: "create-skill",
				source: "bundled",
				valid: true,
				userInvocable: true,
				locked: false,
			},
			{
				name: "review",
				source: "bundled",
				valid: true,
				userInvocable: true,
				locked: false,
			},
			{
				name: "bundled-locked-system",
				source: "bundled",
				valid: true,
				userInvocable: true,
				locked: true,
			},
			{
				name: "project-sf-command-surface",
				source: "project",
				valid: true,
				userInvocable: true,
				locked: false,
			},
		]);
		// Locked skills are invisible. Only create-skill is a default bundled
		// user skill; project/user skills can still appear.
		const names = visible.map((s) => s.name).sort();
		expect(names).toContain("create-skill");
		expect(names).not.toContain("review");
		expect(names).toContain("project-sf-command-surface");
		expect(names).not.toContain("bundled-locked-system");
	});

	test("loadSkills_default_catalog_exposes_only_create_skill_as_bundled_user_skill", () => {
		const visible = getUserInvocableSkills(
			loadSkills(tmpDir, {
				includeBundled: true,
			}),
		).filter((s) => s.source === "bundled");

		expect(visible.map((s) => s.name)).toEqual(["create-skill"]);
	});

	test("loadSkills_workflow_skills_are_hidden_system_records", () => {
		const skills = loadSkills(tmpDir, {
			includeBundled: false,
			includeWorkflow: true,
		});
		const workflow = skills.filter((s) => s.source === "workflow");
		const visible = getUserInvocableSkills(skills);

		expect(workflow.length).toBeGreaterThan(0);
		expect(workflow.every((s) => s.valid)).toBe(true);
		expect(workflow.every((s) => s.userInvocable === false)).toBe(true);
		expect(workflow.some((s) => s.name === "sf-repo-orientation")).toBe(true);
		expect(visible.some((s) => s.source === "workflow")).toBe(false);
	});

	test("buildSkillRecord_sets_locked_from_frontmatter", () => {
		const locked = buildSkillRecord(
			"/p",
			{ name: "x", description: "y", locked: true },
			"",
		);
		expect(locked.locked).toBe(true);
		const unlocked = buildSkillRecord(
			"/p",
			{ name: "x", description: "y" },
			"",
		);
		expect(unlocked.locked).toBe(false);
	});

	test("project_skill_can_use_name_that_matches_hidden_workflow_pattern", () => {
		// Hidden workflow patterns are product-owned and not part of the user
		// skill catalog, so `.agents/skills/observe-first` remains a normal
		// project skill name.
		createSkill("observe-first");

		const skills = loadSkills(tmpDir, { includeBundled: false });
		const observeFirst = skills.filter((s) => s.name === "observe-first");

		expect(observeFirst).toHaveLength(1);
		expect(observeFirst[0].source).toBe("project");
		expect(observeFirst[0].locked).toBe(false);
	});
});

describe("skill preference resolution", () => {
	test("getSkillSearchDirs_prefers_project_over_user_for_bare_names", () => {
		const dirs = getSkillSearchDirs("/repo");

		expect(dirs.map((d) => d.method).slice(0, 2)).toEqual([
			"project-skill",
			"user-skill",
		]);
		expect(dirs[0].dir).toBe(join("/repo", ".agents", "skills"));
		expect(dirs.some((d) => d.dir.includes(".claude"))).toBe(false);
	});
});
