import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, rm, readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { installSkillAtomic, removeSkillAtomic, listInstalledSkillNames } from '../../src/utils/skills-disk.js';

describe('skills-disk', () => {
  let tmp: string;

  beforeEach(async () => {
    tmp = await mkdtemp(join(tmpdir(), 'mcpctl-skills-disk-'));
  });

  afterEach(async () => {
    await rm(tmp, { recursive: true, force: true });
  });

  it('writes SKILL.md and aux files atomically', async () => {
    const target = join(tmp, 'foo');
    const states = await installSkillAtomic(target, {
      content: '# Foo skill',
      files: { 'scripts/setup.sh': '#!/bin/sh\necho hi' },
    });
    expect(await readFile(join(target, 'SKILL.md'), 'utf-8')).toBe('# Foo skill');
    expect(await readFile(join(target, 'scripts/setup.sh'), 'utf-8')).toBe('#!/bin/sh\necho hi');
    expect(states['SKILL.md']).toBeDefined();
    expect(states['SKILL.md'].sha256).toMatch(/^sha256:/);
    expect(states['SKILL.md'].size).toBe('# Foo skill'.length);
    expect(states['scripts/setup.sh']).toBeDefined();
  });

  it('replaces an existing tree without leaving partial state', async () => {
    const target = join(tmp, 'foo');
    await installSkillAtomic(target, { content: 'v1' });
    await installSkillAtomic(target, {
      content: 'v2',
      files: { 'extra.md': 'extra' },
    });
    expect(await readFile(join(target, 'SKILL.md'), 'utf-8')).toBe('v2');
    expect(await readFile(join(target, 'extra.md'), 'utf-8')).toBe('extra');
    // No staging or trash dirs left behind.
    const entries = await readdir(tmp);
    expect(entries.filter((e) => e.includes('mcpctl-staging') || e.includes('mcpctl-trash'))).toHaveLength(0);
  });

  it('rejects path-escape attempts', async () => {
    const target = join(tmp, 'foo');
    await expect(installSkillAtomic(target, {
      content: 'x',
      files: { '../escaped': 'bad' },
    })).rejects.toThrow(/escapes install dir/);
  });

  it('rejects absolute paths in files{}', async () => {
    const target = join(tmp, 'foo');
    await expect(installSkillAtomic(target, {
      content: 'x',
      files: { '/etc/passwd-like': 'bad' },
    })).rejects.toThrow(/escapes install dir/);
  });

  it('removes a skill atomically', async () => {
    const target = join(tmp, 'foo');
    await installSkillAtomic(target, { content: 'x' });
    await removeSkillAtomic(target);
    expect((await readdir(tmp)).filter((n) => n === 'foo')).toHaveLength(0);
  });

  it('remove is a no-op when target does not exist', async () => {
    await expect(removeSkillAtomic(join(tmp, 'never-existed'))).resolves.toBeUndefined();
  });

  it('listInstalledSkillNames ignores staging/trash artifacts', async () => {
    const skillsDir = join(tmp, 'skills-root');
    await mkdir(skillsDir, { recursive: true });
    await mkdir(join(skillsDir, 'real-skill'), { recursive: true });
    await mkdir(join(skillsDir, 'real-skill.mcpctl-staging-1234'), { recursive: true });
    await mkdir(join(skillsDir, 'something.mcpctl-trash-9999'), { recursive: true });
    await writeFile(join(skillsDir, 'real-skill', 'SKILL.md'), 'x');

    const names = await listInstalledSkillNames(skillsDir);
    expect(names).toEqual(['real-skill']);
  });
});
