import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { SKILL_FILE_NAME } from "../../constants/general.js";
import { setupTestDirectory } from "../../test-utils/test-directories.js";
import { ensureDir, writeFileBuffer, writeFileContent } from "../../utils/file.js";
import {
  ClaudecodeSkill,
  type ClaudecodeSkillFrontmatter,
  ClaudecodeSkillFrontmatterSchema,
} from "./claudecode-skill.js";
import { RulesyncSkill, type RulesyncSkillFrontmatterInput } from "./rulesync-skill.js";

describe("ClaudecodeSkill", () => {
  let testDir: string;
  let cleanup: () => Promise<void>;

  beforeEach(async () => {
    ({ testDir, cleanup } = await setupTestDirectory());
    vi.spyOn(process, "cwd").mockReturnValue(testDir);
  });

  afterEach(async () => {
    await cleanup();
    vi.restoreAllMocks();
  });

  describe("constructor", () => {
    it("should create a ClaudecodeSkill with valid frontmatter and body", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "test-skill",
        description: "Test skill description",
      };

      const skill = new ClaudecodeSkill({
        dirName: "test-skill",
        frontmatter,
        body: "This is a test skill body",
        otherFiles: [],
      });

      expect(skill.getFrontmatter()).toEqual(frontmatter);
      expect(skill.getBody()).toBe("This is a test skill body");
      expect(skill.getOtherFiles()).toEqual([]);
    });

    it("should validate frontmatter by default", () => {
      const invalidFrontmatter = {
        name: 123, // Should be string
        description: true, // Should be string
      } as any;

      expect(() => {
        const skill = new ClaudecodeSkill({
          dirName: "invalid-skill",
          frontmatter: invalidFrontmatter,
          body: "Test body",
          otherFiles: [],
        });
        return skill;
      }).toThrow();
    });

    it("should skip validation when validate is false", () => {
      const invalidFrontmatter = {
        name: 123,
        description: true,
      } as any;

      expect(() => {
        const skill = new ClaudecodeSkill({
          dirName: "invalid-skill",
          frontmatter: invalidFrontmatter,
          body: "Test body",
          otherFiles: [],
          validate: false,
        });
        return skill;
      }).not.toThrow();
    });

    it("should handle allowed-tools configuration", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "restricted-skill",
        description: "Skill with tool restrictions",
        "allowed-tools": ["Bash", "Read", "Write"],
      };

      const skill = new ClaudecodeSkill({
        dirName: "restricted-skill",
        frontmatter,
        body: "Restricted skill body",
        otherFiles: [],
      });

      expect(skill.getFrontmatter()["allowed-tools"]).toEqual(["Bash", "Read", "Write"]);
    });

    it("should use default relativeDirPath", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "test-skill",
        description: "Test skill",
      };

      const skill = new ClaudecodeSkill({
        outputRoot: testDir,
        dirName: "test-skill",
        frontmatter,
        body: "Test body",
      });

      expect(skill.getRelativeDirPath()).toBe(join(".claude", "skills"));
    });

    it("should accept custom relativeDirPath", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "test-skill",
        description: "Test skill",
      };

      const skill = new ClaudecodeSkill({
        outputRoot: testDir,
        relativeDirPath: join("custom", "skills"),
        dirName: "test-skill",
        frontmatter,
        body: "Test body",
      });

      expect(skill.getRelativeDirPath()).toBe(join("custom", "skills"));
    });

    it("should support global mode", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "global-skill",
        description: "Global skill",
      };

      const skill = new ClaudecodeSkill({
        dirName: "global-skill",
        frontmatter,
        body: "Global skill body",
        global: true,
      });

      expect(skill.getGlobal()).toBe(true);
    });
  });

  describe("validate", () => {
    it("should validate successfully with valid frontmatter", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "valid-skill",
        description: "Valid skill description",
      };

      const skill = new ClaudecodeSkill({
        dirName: "valid-skill",
        frontmatter,
        body: "Valid body",
        validate: false,
      });

      const result = skill.validate();
      expect(result.success).toBe(true);
    });

    it("should fail validation with invalid frontmatter", () => {
      const invalidFrontmatter = {
        name: 123,
        description: true,
      } as any;

      const skill = new ClaudecodeSkill({
        dirName: "invalid-skill",
        frontmatter: invalidFrontmatter,
        body: "Test body",
        validate: false,
      });

      const result = skill.validate();
      expect(result.success).toBe(false);
      expect(result.error).toBeInstanceOf(Error);
    });

    it("should fail validation when mainFile is undefined", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "test-skill",
        description: "Test skill",
      };

      const skill = new ClaudecodeSkill({
        dirName: "test-skill",
        frontmatter,
        body: "Test body",
        validate: false,
      });

      // Manually set mainFile to undefined to test this edge case
      (skill as any).mainFile = undefined;

      const result = skill.validate();
      expect(result.success).toBe(false);
      expect(result.error?.message).toContain("SKILL.md file does not exist");
    });
  });

  describe("getSettablePaths", () => {
    it("should return default paths", () => {
      const paths = ClaudecodeSkill.getSettablePaths();
      expect(paths.relativeDirPath).toBe(join(".claude", "skills"));
      expect(paths.alternativeSkillRoots).toEqual([join(".claude", "scheduled-tasks")]);
    });

    it("should return same paths for global mode", () => {
      const paths = ClaudecodeSkill.getSettablePaths({ global: true });
      expect(paths.relativeDirPath).toBe(join(".claude", "skills"));
      expect(paths.alternativeSkillRoots).toEqual([join(".claude", "scheduled-tasks")]);
    });
  });

  describe("toRulesyncSkill", () => {
    it("should convert to RulesyncSkill without allowed-tools", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "test-skill",
        description: "Test description",
      };

      const skill = new ClaudecodeSkill({
        dirName: "test-skill",
        frontmatter,
        body: "Test body",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

      expect(rulesyncFrontmatter.name).toBe("test-skill");
      expect(rulesyncFrontmatter.description).toBe("Test description");
      expect(rulesyncFrontmatter.claudecode).toBeUndefined();
      expect(rulesyncSkill.getBody()).toBe("Test body");
    });

    it("should convert to RulesyncSkill with allowed-tools", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "restricted-skill",
        description: "Restricted skill",
        "allowed-tools": ["Bash", "Read"],
      };

      const skill = new ClaudecodeSkill({
        dirName: "restricted-skill",
        frontmatter,
        body: "Restricted body",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

      expect(rulesyncFrontmatter.name).toBe("restricted-skill");
      expect(rulesyncFrontmatter.description).toBe("Restricted skill");
      expect(rulesyncFrontmatter.claudecode).toEqual({
        "allowed-tools": ["Bash", "Read"],
      });
    });

    it("should convert to RulesyncSkill with model", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "model-skill",
        description: "Skill with model",
        model: "opus",
      };

      const skill = new ClaudecodeSkill({
        dirName: "model-skill",
        frontmatter,
        body: "Model body",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

      expect(rulesyncFrontmatter.claudecode).toEqual({ model: "opus" });
    });

    it("should convert to RulesyncSkill with both model and allowed-tools", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "full-skill",
        description: "Full skill",
        model: "haiku",
        "allowed-tools": ["Bash"],
      };

      const skill = new ClaudecodeSkill({
        dirName: "full-skill",
        frontmatter,
        body: "Full body",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

      expect(rulesyncFrontmatter.claudecode).toEqual({
        model: "haiku",
        "allowed-tools": ["Bash"],
      });
    });

    it("should convert to RulesyncSkill with disable-model-invocation true", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "no-invoke-skill",
        description: "Skill with disabled invocation",
        "disable-model-invocation": true,
      };

      const skill = new ClaudecodeSkill({
        dirName: "no-invoke-skill",
        frontmatter,
        body: "No invoke body",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

      expect(rulesyncFrontmatter.claudecode).toEqual({
        "disable-model-invocation": true,
      });
    });

    it("should convert to RulesyncSkill with disable-model-invocation false", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "invoke-skill",
        description: "Skill with explicit false",
        "disable-model-invocation": false,
      };

      const skill = new ClaudecodeSkill({
        dirName: "invoke-skill",
        frontmatter,
        body: "Invoke body",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

      expect(rulesyncFrontmatter.claudecode).toEqual({
        "disable-model-invocation": false,
      });
    });

    it("should convert to RulesyncSkill with paths as string", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "paths-string-skill",
        description: "Skill with comma-separated paths",
        paths: "src/**/*.ts,test/**/*.ts",
      };

      const skill = new ClaudecodeSkill({
        dirName: "paths-string-skill",
        frontmatter,
        body: "Paths string body",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

      expect(rulesyncFrontmatter.claudecode).toEqual({
        paths: "src/**/*.ts,test/**/*.ts",
      });
    });

    it("should convert to RulesyncSkill with paths as array", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "paths-array-skill",
        description: "Skill with paths list",
        paths: ["src/**/*.ts", "test/**/*.ts"],
      };

      const skill = new ClaudecodeSkill({
        dirName: "paths-array-skill",
        frontmatter,
        body: "Paths array body",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

      expect(rulesyncFrontmatter.claudecode).toEqual({
        paths: ["src/**/*.ts", "test/**/*.ts"],
      });
    });

    it("should preserve other files during conversion", () => {
      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "test-skill",
        description: "Test skill",
      };

      const otherFiles = [
        {
          relativeFilePathToDirPath: "helper.ts",
          fileBuffer: Buffer.from("helper code"),
        },
      ];

      const skill = new ClaudecodeSkill({
        dirName: "test-skill",
        frontmatter,
        body: "Test body",
        otherFiles,
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      expect(rulesyncSkill.getOtherFiles()).toEqual(otherFiles);
    });

    it("should mark scheduled-task when converting scheduled-task directory", () => {
      const skill = new ClaudecodeSkill({
        dirName: "weekly-review",
        relativeDirPath: join(".claude", "scheduled-tasks"),
        frontmatter: {
          name: "weekly-review",
          description: "Weekly review task",
        },
        body: "Run weekly review",
      });

      const rulesyncSkill = skill.toRulesyncSkill();
      expect(rulesyncSkill.getFrontmatter().claudecode).toEqual({
        "scheduled-task": true,
      });
    });
  });

  describe("fromRulesyncSkill", () => {
    it("should convert from RulesyncSkill without claudecode config", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "test-skill",
        description: "Test description",
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "test-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Test body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      const frontmatter = claudecodeSkill.getFrontmatter();

      expect(frontmatter.name).toBe("test-skill");
      expect(frontmatter.description).toBe("Test description");
      expect(frontmatter["allowed-tools"]).toBeUndefined();
    });

    it("should convert from RulesyncSkill with claudecode config", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "restricted-skill",
        description: "Restricted skill",
        claudecode: {
          "allowed-tools": ["Bash", "Read"],
        },
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "restricted-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Restricted body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      const frontmatter = claudecodeSkill.getFrontmatter();

      expect(frontmatter.name).toBe("restricted-skill");
      expect(frontmatter.description).toBe("Restricted skill");
      expect(frontmatter["allowed-tools"]).toEqual(["Bash", "Read"]);
    });

    it("should convert from RulesyncSkill with claudecode model", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "model-skill",
        description: "Skill with model",
        claudecode: { model: "sonnet" },
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "model-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Model body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      expect(claudecodeSkill.getFrontmatter().model).toBe("sonnet");
    });

    it("should convert from RulesyncSkill with both model and allowed-tools", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "full-skill",
        description: "Full skill",
        claudecode: { model: "haiku", "allowed-tools": ["Bash"] },
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "full-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Full body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      const fm = claudecodeSkill.getFrontmatter();
      expect(fm.model).toBe("haiku");
      expect(fm["allowed-tools"]).toEqual(["Bash"]);
    });

    it("should convert from RulesyncSkill with disable-model-invocation", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "no-invoke-skill",
        description: "Skill with disabled invocation",
        claudecode: { "disable-model-invocation": true },
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "no-invoke-skill",
        frontmatter: rulesyncFrontmatter,
        body: "No invoke body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(true);
    });

    it("should convert from RulesyncSkill with disable-model-invocation false", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "invoke-skill",
        description: "Skill with explicit false",
        claudecode: { "disable-model-invocation": false },
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "invoke-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Invoke body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(false);
    });

    it("should convert from RulesyncSkill with paths as string", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "paths-string-skill",
        description: "Skill with comma-separated paths",
        claudecode: { paths: "src/**/*.ts,test/**/*.ts" },
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "paths-string-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Paths string body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      expect(claudecodeSkill.getFrontmatter().paths).toBe("src/**/*.ts,test/**/*.ts");
    });

    it("should convert from RulesyncSkill with paths as array", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "paths-array-skill",
        description: "Skill with paths list",
        claudecode: { paths: ["src/**/*.ts", "test/**/*.ts"] },
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "paths-array-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Paths array body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      expect(claudecodeSkill.getFrontmatter().paths).toEqual(["src/**/*.ts", "test/**/*.ts"]);
    });

    it("should set correct relativeDirPath", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "test-skill",
        description: "Test skill",
      };

      const rulesyncSkill = new RulesyncSkill({
        outputRoot: testDir,
        dirName: "test-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Test body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });

      expect(claudecodeSkill.getRelativeDirPath()).toBe(join(".claude", "skills"));
    });

    it("should preserve other files during conversion", () => {
      const otherFiles = [
        {
          relativeFilePathToDirPath: "helper.ts",
          fileBuffer: Buffer.from("helper code"),
        },
      ];

      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "test-skill",
        description: "Test skill",
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "test-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Test body",
        otherFiles,
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      expect(claudecodeSkill.getOtherFiles()).toEqual(otherFiles);
    });

    it("should skip validation when validate is false", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "valid-skill",
        description: "Valid skill",
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "valid-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Test body",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({
        rulesyncSkill,
        validate: false,
      });

      // Even with validate=false, the skill should be created
      expect(claudecodeSkill).toBeInstanceOf(ClaudecodeSkill);
    });

    it("should support global mode", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "global-skill",
        description: "Global skill",
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "global-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Global body",
        global: true,
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({
        rulesyncSkill,
        global: true,
      });

      expect(claudecodeSkill.getGlobal()).toBe(true);
    });

    it("should route scheduled-task skills to scheduled-tasks directory", () => {
      const rulesyncSkill = new RulesyncSkill({
        dirName: "weekly-review",
        frontmatter: {
          name: "weekly-review",
          description: "Weekly review task",
          claudecode: {
            "scheduled-task": true,
          },
        },
        body: "Run weekly review",
      });

      const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({
        rulesyncSkill,
        global: true,
      });

      expect(claudecodeSkill.getRelativeDirPath()).toBe(join(".claude", "scheduled-tasks"));
    });
  });

  describe("isTargetedByRulesyncSkill", () => {
    it("should always return true for any RulesyncSkill", () => {
      const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
        name: "test-skill",
        description: "Test skill",
      };

      const rulesyncSkill = new RulesyncSkill({
        dirName: "test-skill",
        frontmatter: rulesyncFrontmatter,
        body: "Test body",
      });

      expect(ClaudecodeSkill.isTargetedByRulesyncSkill(rulesyncSkill)).toBe(true);
    });

    it("should target scheduled-task even when targets does not include claudecode", () => {
      const rulesyncSkill = new RulesyncSkill({
        dirName: "scheduled-only",
        frontmatter: {
          name: "scheduled-only",
          description: "Scheduled task",
          targets: ["cursor"],
          claudecode: {
            "scheduled-task": true,
          },
        },
        body: "Scheduled body",
      });

      expect(ClaudecodeSkill.isTargetedByRulesyncSkill(rulesyncSkill)).toBe(true);
    });
  });

  describe("fromDir", () => {
    it("should load skill from directory", async () => {
      const skillDir = join(testDir, ".claude", "skills", "test-skill");
      await ensureDir(skillDir);

      const frontmatter: ClaudecodeSkillFrontmatter = {
        name: "test-skill",
        description: "Test skill description",
      };

      const content = `---
name: test-skill
description: Test skill description
---

This is the skill body.`;

      await writeFileContent(join(skillDir, SKILL_FILE_NAME), content);

      const skill = await ClaudecodeSkill.fromDir({
        outputRoot: testDir,
        dirName: "test-skill",
      });

      expect(skill.getFrontmatter()).toEqual(frontmatter);
      expect(skill.getBody()).toBe("This is the skill body.");
    });

    it("should load skill with allowed-tools", async () => {
      const skillDir = join(testDir, ".claude", "skills", "restricted-skill");
      await ensureDir(skillDir);

      const content = `---
name: restricted-skill
description: Skill with tool restrictions
allowed-tools:
  - Bash
  - Read
  - Write
---

This skill has tool restrictions.`;

      await writeFileContent(join(skillDir, SKILL_FILE_NAME), content);

      const skill = await ClaudecodeSkill.fromDir({
        outputRoot: testDir,
        dirName: "restricted-skill",
      });

      expect(skill.getFrontmatter()["allowed-tools"]).toEqual(["Bash", "Read", "Write"]);
    });

    it("should load skill with model", async () => {
      const skillDir = join(testDir, ".claude", "skills", "model-skill");
      await ensureDir(skillDir);

      const content = `---
name: model-skill
description: Skill with model
model: sonnet
---

This skill uses a specific model.`;

      await writeFileContent(join(skillDir, SKILL_FILE_NAME), content);

      const skill = await ClaudecodeSkill.fromDir({
        outputRoot: testDir,
        dirName: "model-skill",
      });

      expect(skill.getFrontmatter().model).toBe("sonnet");
    });

    it("should load skill with other files", async () => {
      const skillDir = join(testDir, ".claude", "skills", "multi-file-skill");
      await ensureDir(skillDir);

      const content = `---
name: multi-file-skill
description: Skill with multiple files
---

Main skill content.`;

      await writeFileContent(join(skillDir, SKILL_FILE_NAME), content);

      await writeFileBuffer(
        join(skillDir, "helper.ts"),
        Buffer.from("export function helper() {}"),
      );

      const skill = await ClaudecodeSkill.fromDir({
        outputRoot: testDir,
        dirName: "multi-file-skill",
      });

      const otherFiles = skill.getOtherFiles();
      expect(otherFiles).toHaveLength(1);
      expect(otherFiles[0]?.relativeFilePathToDirPath).toBe("helper.ts");
      expect(otherFiles[0]?.fileBuffer.toString()).toBe("export function helper() {}");
    });

    it("should throw error when SKILL.md does not exist", async () => {
      const skillDir = join(testDir, ".claude", "skills", "missing-skill");
      await ensureDir(skillDir);

      await expect(
        ClaudecodeSkill.fromDir({
          outputRoot: testDir,
          dirName: "missing-skill",
        }),
      ).rejects.toThrow("SKILL.md not found");
    });

    it("should throw error with invalid frontmatter", async () => {
      const skillDir = join(testDir, ".claude", "skills", "invalid-skill");
      await ensureDir(skillDir);

      const content = `---
name: 123
description: true
---

Invalid frontmatter.`;

      await writeFileContent(join(skillDir, SKILL_FILE_NAME), content);

      await expect(
        ClaudecodeSkill.fromDir({
          outputRoot: testDir,
          dirName: "invalid-skill",
        }),
      ).rejects.toThrow("Invalid frontmatter");
    });

    it("should use custom relativeDirPath when provided", async () => {
      const customPath = join("custom", "skills");
      const skillDir = join(testDir, customPath, "custom-skill");
      await ensureDir(skillDir);

      const content = `---
name: custom-skill
description: Custom path skill
---

Custom path content.`;

      await writeFileContent(join(skillDir, SKILL_FILE_NAME), content);

      const skill = await ClaudecodeSkill.fromDir({
        outputRoot: testDir,
        relativeDirPath: customPath,
        dirName: "custom-skill",
      });

      expect(skill.getRelativeDirPath()).toBe(customPath);
    });

    it("should support global mode", async () => {
      const skillDir = join(testDir, ".claude", "skills", "global-skill");
      await ensureDir(skillDir);

      const content = `---
name: global-skill
description: Global mode skill
---

Global skill content.`;

      await writeFileContent(join(skillDir, SKILL_FILE_NAME), content);

      const skill = await ClaudecodeSkill.fromDir({
        outputRoot: testDir,
        dirName: "global-skill",
        global: true,
      });

      expect(skill.getGlobal()).toBe(true);
    });
  });

  describe("ClaudecodeSkillFrontmatterSchema", () => {
    it("should validate valid frontmatter", () => {
      const validFrontmatter = {
        name: "test-skill",
        description: "Test description",
      };

      const result = ClaudecodeSkillFrontmatterSchema.safeParse(validFrontmatter);
      expect(result.success).toBe(true);
    });

    it("should validate frontmatter with allowed-tools", () => {
      const validFrontmatter = {
        name: "test-skill",
        description: "Test description",
        "allowed-tools": ["Bash", "Read"],
      };

      const result = ClaudecodeSkillFrontmatterSchema.safeParse(validFrontmatter);
      expect(result.success).toBe(true);
    });

    it("should reject frontmatter without name", () => {
      const invalidFrontmatter = {
        description: "Test description",
      };

      const result = ClaudecodeSkillFrontmatterSchema.safeParse(invalidFrontmatter);
      expect(result.success).toBe(false);
    });

    it("should reject frontmatter without description", () => {
      const invalidFrontmatter = {
        name: "test-skill",
      };

      const result = ClaudecodeSkillFrontmatterSchema.safeParse(invalidFrontmatter);
      expect(result.success).toBe(false);
    });

    it("should reject invalid allowed-tools type", () => {
      const invalidFrontmatter = {
        name: "test-skill",
        description: "Test description",
        "allowed-tools": "not-an-array",
      };

      const result = ClaudecodeSkillFrontmatterSchema.safeParse(invalidFrontmatter);
      expect(result.success).toBe(false);
    });

    it("should validate frontmatter with model field", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        model: "opus",
      });
      expect(result.success).toBe(true);
    });

    it("should validate frontmatter with disable-model-invocation true", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        "disable-model-invocation": true,
      });
      expect(result.success).toBe(true);
    });

    it("should validate frontmatter with disable-model-invocation false", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        "disable-model-invocation": false,
      });
      expect(result.success).toBe(true);
    });

    it("should reject non-boolean disable-model-invocation value", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        "disable-model-invocation": "yes",
      });
      expect(result.success).toBe(false);
    });

    it("should reject non-string model value", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        model: 123,
      });
      expect(result.success).toBe(false);
    });

    it("should validate frontmatter with paths as string", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        paths: "src/**/*.ts,test/**/*.ts",
      });
      expect(result.success).toBe(true);
    });

    it("should validate frontmatter with paths as array", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        paths: ["src/**/*.ts", "test/**/*.ts"],
      });
      expect(result.success).toBe(true);
    });

    it("should reject paths with invalid type", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        paths: 123,
      });
      expect(result.success).toBe(false);
    });

    it("should reject paths with array containing non-strings", () => {
      const result = ClaudecodeSkillFrontmatterSchema.safeParse({
        name: "test-skill",
        description: "Test",
        paths: ["src/**/*.ts", 42],
      });
      expect(result.success).toBe(false);
    });
  });

  describe("round-trip conversion", () => {
    it("should preserve disable-model-invocation through round-trip", () => {
      const originalFrontmatter: ClaudecodeSkillFrontmatter = {
        name: "round-trip-skill",
        description: "Round trip test",
        "disable-model-invocation": true,
      };

      const original = new ClaudecodeSkill({
        dirName: "round-trip-skill",
        frontmatter: originalFrontmatter,
        body: "Round trip body",
      });

      const rulesyncSkill = original.toRulesyncSkill();
      const restored = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });

      expect(restored.getFrontmatter()["disable-model-invocation"]).toBe(true);
    });

    it("should preserve disable-model-invocation false through round-trip", () => {
      const originalFrontmatter: ClaudecodeSkillFrontmatter = {
        name: "round-trip-skill",
        description: "Round trip test",
        "disable-model-invocation": false,
      };

      const original = new ClaudecodeSkill({
        dirName: "round-trip-skill",
        frontmatter: originalFrontmatter,
        body: "Round trip body",
      });

      const rulesyncSkill = original.toRulesyncSkill();
      const restored = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });

      expect(restored.getFrontmatter()["disable-model-invocation"]).toBe(false);
    });

    it("should preserve paths string through round-trip", () => {
      const originalFrontmatter: ClaudecodeSkillFrontmatter = {
        name: "round-trip-skill",
        description: "Round trip test",
        paths: "src/**/*.ts,test/**/*.ts",
      };

      const original = new ClaudecodeSkill({
        dirName: "round-trip-skill",
        frontmatter: originalFrontmatter,
        body: "Round trip body",
      });

      const rulesyncSkill = original.toRulesyncSkill();
      const restored = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });

      expect(restored.getFrontmatter().paths).toBe("src/**/*.ts,test/**/*.ts");
    });

    it("should preserve paths array through round-trip", () => {
      const originalFrontmatter: ClaudecodeSkillFrontmatter = {
        name: "round-trip-skill",
        description: "Round trip test",
        paths: ["src/**/*.ts", "test/**/*.ts"],
      };

      const original = new ClaudecodeSkill({
        dirName: "round-trip-skill",
        frontmatter: originalFrontmatter,
        body: "Round trip body",
      });

      const rulesyncSkill = original.toRulesyncSkill();
      const restored = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });

      expect(restored.getFrontmatter().paths).toEqual(["src/**/*.ts", "test/**/*.ts"]);
    });

    it("should preserve model through ClaudecodeSkill -> RulesyncSkill -> ClaudecodeSkill", () => {
      const originalFrontmatter: ClaudecodeSkillFrontmatter = {
        name: "round-trip-skill",
        description: "Round trip test",
        model: "sonnet",
        "allowed-tools": ["Bash", "Read"],
      };

      const original = new ClaudecodeSkill({
        dirName: "round-trip-skill",
        frontmatter: originalFrontmatter,
        body: "Round trip body",
      });

      const rulesyncSkill = original.toRulesyncSkill();
      const restored = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
      const restoredFm = restored.getFrontmatter();

      expect(restoredFm.name).toBe("round-trip-skill");
      expect(restoredFm.description).toBe("Round trip test");
      expect(restoredFm.model).toBe("sonnet");
      expect(restoredFm["allowed-tools"]).toEqual(["Bash", "Read"]);
    });
  });
});
