import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SkillsRegistryService } from '../../../src/context/skills/skills-registry.service.js';

describe('SkillsRegistryService', () => {
  let service: SkillsRegistryService;
  let tempDir: string;

  beforeEach(async () => {
    tempDir = await mkdtemp(join(tmpdir(), 'skills-registry-'));
    service = new SkillsRegistryService({ skillsDir: tempDir });
  });

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

  const writeSkill = async (dirName: string, body: string) => {
    const dir = join(tempDir, dirName);
    await mkdir(dir, { recursive: true });
    await writeFile(join(dir, 'SKILL.md'), body, 'utf-8');
  };

  describe('parseFrontmatter', () => {
    it('parses name and description', () => {
      const frontmatter = service.parseFrontmatter('---\nname: foo\ndescription: Bar baz\n---\n\n# body');
      expect(frontmatter).toEqual({ name: 'foo', description: 'Bar baz' });
    });

    it('supports wrapped description continuation lines', () => {
      const frontmatter = service.parseFrontmatter(
        ['---', 'name: sl', 'description: Line one', '  continuation of the description.', '---', '', '# body'].join(
          '\n',
        ),
      );
      expect(frontmatter.name).toBe('sl');
      expect(frontmatter.description).toContain('Line one');
      expect(frontmatter.description).toContain('continuation');
    });

    it('returns empty fields when no frontmatter block', () => {
      expect(service.parseFrontmatter('# just a heading')).toEqual({});
    });
  });

  describe('stripFrontmatter', () => {
    it('removes the frontmatter block and leading blank line', () => {
      const body = '---\nname: x\ndescription: y\n---\n\n# Hello\n\nparagraph';
      expect(service.stripFrontmatter(body)).toBe('# Hello\n\nparagraph');
    });

    it('is a no-op when no frontmatter exists', () => {
      expect(service.stripFrontmatter('# hello')).toBe('# hello');
    });
  });

  describe('discoverSkills', () => {
    it('returns an empty map when the directory does not exist', async () => {
      const catalog = await service.discoverSkills(join(tempDir, 'missing'));
      expect(catalog.size).toBe(0);
    });

    it('discovers valid skills and skips invalid ones', async () => {
      await writeSkill('sl', '---\nname: sl\ndescription: Semantic layer.\n---\n\n# SL');
      await writeSkill('wiki_capture', '---\nname: wiki_capture\ndescription: Wiki capture.\n---\n\n# KC');
      await writeSkill('broken', '# no frontmatter at all');
      await mkdir(join(tempDir, 'not_a_skill'), { recursive: true });

      const catalog = await service.discoverSkills(tempDir);
      expect(catalog.size).toBe(2);
      expect(catalog.get('sl')?.name).toBe('sl');
      expect(catalog.get('wiki_capture')?.description).toContain('Wiki capture');
      expect(catalog.has('broken')).toBe(false);
    });
  });

  describe('buildSkillsPrompt', () => {
    it('formats bullet list with name and description', () => {
      const output = service.buildSkillsPrompt([
        { name: 'sl', description: 'Semantic layer.', path: '/tmp/sl' },
        { name: 'wiki_capture', description: 'Wiki capture.', path: '/tmp/kc' },
      ]);
      expect(output).toContain('- sl: Semantic layer.');
      expect(output).toContain('- wiki_capture: Wiki capture.');
      expect(output).toContain('Use the `load_skill` tool');
    });

    it('returns empty string when no skills are available', () => {
      expect(service.buildSkillsPrompt([])).toBe('');
    });

    it('appends the async capture note for the research caller', () => {
      const output = service.buildSkillsPrompt(
        [{ name: 'sl', description: 'Semantic layer.', path: '/tmp/sl' }],
        'research',
      );
      expect(output).toContain('captured automatically by a post-turn memory agent');
      expect(output).toContain('Focus on answering, not on saving');
    });

    it('does not append the note for memory_agent caller', () => {
      const output = service.buildSkillsPrompt(
        [{ name: 'sl_capture', description: 'Capture skill.', path: '/tmp/cap' }],
        'memory_agent',
      );
      expect(output).not.toContain('captured automatically by a post-turn memory agent');
    });
  });

  describe('parseFrontmatter callers field', () => {
    it('parses inline-array form', () => {
      const frontmatter = service.parseFrontmatter('---\nname: x\ndescription: y\ncallers: [memory_agent]\n---\n');
      expect(frontmatter.callers).toEqual(['memory_agent']);
    });

    it('parses comma-separated form', () => {
      const frontmatter = service.parseFrontmatter('---\nname: x\ndescription: y\ncallers: research, memory_agent\n---\n');
      expect(frontmatter.callers).toEqual(['research', 'memory_agent']);
    });

    it('returns undefined when callers is absent', () => {
      const frontmatter = service.parseFrontmatter('---\nname: x\ndescription: y\n---\n');
      expect(frontmatter.callers).toBeUndefined();
    });

    it('drops unknown caller names with a warning', () => {
      const frontmatter = service.parseFrontmatter('---\nname: x\ndescription: y\ncallers: [bogus, memory_agent]\n---\n');
      expect(frontmatter.callers).toEqual(['memory_agent']);
    });

    it('returns undefined when the value is empty', () => {
      const frontmatter = service.parseFrontmatter('---\nname: x\ndescription: y\ncallers:\n---\n');
      expect(frontmatter.callers).toBeUndefined();
    });
  });

  describe('listSkills and getSkill caller filter', () => {
    beforeEach(async () => {
      await writeSkill('sl', '---\nname: sl\ndescription: Open to all.\n---\n\n# SL');
      await writeSkill(
        'sl_capture',
        '---\nname: sl_capture\ndescription: Memory-only capture skill.\ncallers: [memory_agent]\n---\n\n# Capture',
      );
      await writeSkill(
        'wiki_capture',
        '---\nname: wiki_capture\ndescription: Wiki capture.\ncallers: [memory_agent]\n---\n\n# KC',
      );
      service = new SkillsRegistryService({ skillsDir: tempDir });
    });

    it('research caller sees only open skills', async () => {
      const skills = await service.listSkills('research');
      expect(skills.map((skill) => skill.name).sort()).toEqual(['sl']);
    });

    it('memory_agent caller sees memory-only and open skills', async () => {
      const skills = await service.listSkills('memory_agent');
      expect(skills.map((skill) => skill.name).sort()).toEqual(['sl', 'sl_capture', 'wiki_capture']);
    });

    it('listSkills with names and caller intersects both filters', async () => {
      const skills = await service.listSkills(['sl', 'sl_capture'], 'research');
      expect(skills.map((skill) => skill.name)).toEqual(['sl']);
    });

    it('getSkill returns null for memory-only skill when caller is research', async () => {
      const skill = await service.getSkill('sl_capture', 'research');
      expect(skill).toBeNull();
    });

    it('getSkill returns the skill when caller has access', async () => {
      const skill = await service.getSkill('sl_capture', 'memory_agent');
      expect(skill?.name).toBe('sl_capture');
    });

    it('getSkill without caller returns the skill regardless of callers field', async () => {
      const skill = await service.getSkill('sl_capture');
      expect(skill?.name).toBe('sl_capture');
    });

  });

  it('discovers skills from additional directories when the primary directory misses', async () => {
    const extraDir = await mkdtemp(join(tmpdir(), 'skills-registry-extra-'));
    try {
      await mkdir(join(extraDir, 'wiki_capture'), { recursive: true });
      await writeFile(
        join(extraDir, 'wiki_capture', 'SKILL.md'),
        [
          '---',
          'name: wiki_capture',
          'description: Packaged knowledge capture skill.',
          'callers: [memory_agent]',
          '---',
          '',
          '# Wiki Capture',
        ].join('\n'),
        'utf-8',
      );
      service = new SkillsRegistryService({ skillsDir: tempDir, additionalSkillDirs: [extraDir] });

      const skills = await service.listSkills(['wiki_capture'], 'memory_agent');

      expect(skills.map((skill) => skill.name)).toEqual(['wiki_capture']);
      expect(skills[0]?.path).toBe(join(extraDir, 'wiki_capture'));
    } finally {
      await rm(extraDir, { recursive: true, force: true });
    }
  });
});
