import * as fs from 'fs';
import * as path from 'path';
import { tmpdir } from 'os';
import { describe, expect, it, afterEach } from 'vitest';
import { SkillCatalog, VALID_SKILL_NAME, type SkillEntry } from './skills.js';

function makeTempDir(): string {
  return fs.mkdtempSync(path.join(tmpdir(), 'skill-test-'));
}

function writeSkill(dir: string, filename: string, content: string): void {
  fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(path.join(dir, filename), content, 'utf-8');
}

const SKILL_A = `---
name: tdd
description: テスト駆動開発の手順
triggers:
  - テスト
  - 新機能
---

## 手順
1. RED: 失敗するテストを書く
2. GREEN: 最小限のコードで通す
3. REFACTOR: 整理
`;

const SKILL_B = `---
name: code-review
description: コードレビューのチェックリスト
---

- セキュリティ
- パフォーマンス
- 可読性
`;

describe('SkillCatalog', () => {
  const dirs: string[] = [];
  afterEach(() => {
    for (const d of dirs) fs.rmSync(d, { recursive: true, force: true });
    dirs.length = 0;
  });

  it('loads system skills from the system skills directory', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);
    writeSkill(systemDir, 'code-review.md', SKILL_B);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    expect(skills).toHaveLength(2);
    expect(skills.map(s => s.name).sort()).toEqual(['code-review', 'tdd']);
  });

  it('parses frontmatter fields correctly', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);
    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    const tdd = skills.find(s => s.name === 'tdd')!;
    expect(tdd.description).toBe('テスト駆動開発の手順');
    expect(tdd.triggers).toEqual(['テスト', '新機能']);
    expect(tdd.source).toBe('system');
  });

  it('merges user skills on top of system skills', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);

    const userSkillDir = path.join(userRoot, 'user1', 'skills');
    const userSkill = `---
name: my-workflow
description: 個人ワークフロー
---
カスタム手順
`;
    writeSkill(userSkillDir, 'my-workflow.md', userSkill);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    expect(skills).toHaveLength(2);
    expect(skills.find(s => s.name === 'my-workflow')?.source).toBe('user');
    expect(skills.find(s => s.name === 'tdd')?.source).toBe('system');
  });

  it('user skill with same name overrides system skill', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);

    const userSkillDir = path.join(userRoot, 'user1', 'skills');
    const overrideSkill = `---
name: tdd
description: カスタム TDD 手順
---
独自のやり方
`;
    writeSkill(userSkillDir, 'tdd.md', overrideSkill);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    const tdd = skills.find(s => s.name === 'tdd')!;
    expect(tdd.description).toBe('カスタム TDD 手順');
    expect(tdd.source).toBe('user');
  });

  it('returns full content via getSkillContent()', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);
    const catalog = new SkillCatalog(systemDir, userRoot);
    const result = catalog.getSkillContent('tdd', 'user1');
    expect(result!.content).toContain('## 手順');
    expect(result!.content).toContain('RED: 失敗するテストを書く');
    expect(result!.dirPath).toBeNull();
  });

  it('returns null for nonexistent skill', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    const catalog = new SkillCatalog(systemDir, userRoot);
    expect(catalog.getSkillContent('nonexistent', 'user1')).toBeNull();
  });

  it('buildIndex() returns markdown index of all skills', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);
    writeSkill(systemDir, 'code-review.md', SKILL_B);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const index = catalog.buildIndex('user1');
    expect(index).toContain('tdd');
    expect(index).toContain('テスト駆動開発の手順');
    expect(index).toContain('code-review');
    expect(index).toContain('コードレビューのチェックリスト');
  });

  it('caches results and invalidate() clears cache', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);
    const catalog = new SkillCatalog(systemDir, userRoot);

    const first = catalog.getForUser('user1');
    expect(first).toHaveLength(1);

    // Add a new skill file — cached result should still be 1
    writeSkill(systemDir, 'code-review.md', SKILL_B);
    const cached = catalog.getForUser('user1');
    expect(cached).toHaveLength(1); // still cached

    // After invalidation, should pick up the new file
    catalog.invalidate('user1');
    const fresh = catalog.getForUser('user1');
    expect(fresh).toHaveLength(2);
  });

  it('handles empty system skills directory gracefully', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    const catalog = new SkillCatalog(systemDir, userRoot);
    expect(catalog.getForUser('user1')).toEqual([]);
    expect(catalog.buildIndex('user1')).toBe('');
  });

  it('buildIndex() respects maxChars budget', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    // Create 20 skills with long descriptions to exceed a small budget
    for (let i = 0; i < 20; i++) {
      const skill = `---
name: skill-${String(i).padStart(2, '0')}
description: This is a fairly long description for skill number ${i} that takes up space in the index
---
Content for skill ${i}
`;
      writeSkill(systemDir, `skill-${String(i).padStart(2, '0')}.md`, skill);
    }

    const catalog = new SkillCatalog(systemDir, userRoot);
    const index = catalog.buildIndex('user1', 200);
    expect(index.length).toBeLessThanOrEqual(200 + 100); // allow overflow for the trailing message
    expect(index).toContain('use ListSkills to see all');
  });

  it('buildIndex() returns full index when under budget', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const index = catalog.buildIndex('user1', 2000);
    expect(index).toContain('tdd');
    expect(index).not.toContain('use ListSkills to see all');
  });

  it('skips files without valid frontmatter', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'bad.md', 'no frontmatter here');
    writeSkill(systemDir, 'tdd.md', SKILL_A);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    // bad.md should be skipped (no name in frontmatter), tdd.md loaded
    expect(skills).toHaveLength(1);
    expect(skills[0].name).toBe('tdd');
  });

  it('sets dirPath for directory-based skills (SKILL.md)', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    const skillDir = path.join(systemDir, 'tdd');
    writeSkill(skillDir, 'SKILL.md', SKILL_A);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    expect(skills).toHaveLength(1);
    const tdd = skills[0];
    expect(tdd.name).toBe('tdd');
    expect(tdd.dirPath).toBe(skillDir);
    expect(tdd.filePath).toBe(path.join(skillDir, 'SKILL.md'));
  });

  it('sets dirPath to null for single-file skills', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    expect(skills).toHaveLength(1);
    expect(skills[0].dirPath).toBeNull();
  });

  it('skips skills with invalid name characters', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    const badSkill = `---\nname: "Bad Name!!"\ndescription: invalid name\n---\nContent\n`;
    writeSkill(systemDir, 'bad-name.md', badSkill);
    writeSkill(systemDir, 'tdd.md', SKILL_A);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    expect(skills).toHaveLength(1);
    expect(skills[0].name).toBe('tdd');
  });

  it('skips symlinked skill files', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    writeSkill(systemDir, 'tdd.md', SKILL_A);
    // Create a symlink to tdd.md
    const symlinkPath = path.join(systemDir, 'linked.md');
    fs.symlinkSync(path.join(systemDir, 'tdd.md'), symlinkPath);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    // Only tdd.md should load; linked.md (symlink) should be skipped
    expect(skills).toHaveLength(1);
    expect(skills[0].name).toBe('tdd');
  });

  it('directory skill wins over same-name flat file', () => {
    const systemDir = makeTempDir();
    const userRoot = makeTempDir();
    dirs.push(systemDir, userRoot);

    // Create flat file version
    const flatSkill = `---\nname: tdd\ndescription: flat file version\n---\nFlat content\n`;
    writeSkill(systemDir, 'tdd.md', flatSkill);

    // Create directory version
    const dirSkill = `---\nname: tdd\ndescription: directory version\n---\nDirectory content\n`;
    const skillDir = path.join(systemDir, 'tdd');
    writeSkill(skillDir, 'SKILL.md', dirSkill);

    const catalog = new SkillCatalog(systemDir, userRoot);
    const skills = catalog.getForUser('user1');
    const tdd = skills.find(s => s.name === 'tdd')!;
    // Directory version should win
    expect(tdd.description).toBe('directory version');
    expect(tdd.dirPath).toBe(skillDir);
  });

  it('VALID_SKILL_NAME regex accepts valid names and rejects invalid ones', () => {
    expect(VALID_SKILL_NAME.test('tdd')).toBe(true);
    expect(VALID_SKILL_NAME.test('code-review')).toBe(true);
    expect(VALID_SKILL_NAME.test('my_skill_123')).toBe(true);
    expect(VALID_SKILL_NAME.test('Bad Name!!')).toBe(false);
    expect(VALID_SKILL_NAME.test('has spaces')).toBe(false);
    expect(VALID_SKILL_NAME.test('UPPERCASE')).toBe(false);
    expect(VALID_SKILL_NAME.test('')).toBe(false);
  });

  describe('getSkillBinds', () => {
    it('returns system bind when systemDir exists', () => {
      const systemDir = makeTempDir();
      const userRoot = makeTempDir();
      dirs.push(systemDir, userRoot);

      writeSkill(systemDir, 'tdd.md', SKILL_A);
      const catalog = new SkillCatalog(systemDir, userRoot);
      const binds = catalog.getSkillBinds('user1');
      expect(binds).toEqual([{ src: systemDir, dest: '/skills' }]);
    });

    it('returns user bind when user skills dir exists', () => {
      const systemDir = makeTempDir();
      const userRoot = makeTempDir();
      dirs.push(systemDir, userRoot);

      const userSkillDir = path.join(userRoot, 'user1', 'skills');
      writeSkill(userSkillDir, 'my-workflow.md', `---\nname: my-workflow\ndescription: test\n---\nContent\n`);

      const catalog = new SkillCatalog(systemDir, userRoot);
      const binds = catalog.getSkillBinds('user1');
      expect(binds).toContainEqual({ src: systemDir, dest: '/skills' });
      expect(binds).toContainEqual({ src: userSkillDir, dest: '/user-skills' });
      expect(binds).toHaveLength(2);
    });

    it('returns empty array when no skill dirs exist', () => {
      const systemDir = path.join(makeTempDir(), 'nonexistent');
      const userRoot = makeTempDir();
      dirs.push(userRoot);

      const catalog = new SkillCatalog(systemDir, userRoot);
      const binds = catalog.getSkillBinds('user1');
      expect(binds).toEqual([]);
    });
  });

  describe('refreshSystem', () => {
    it('clears all user caches and re-scans system skills', () => {
      const systemDir = makeTempDir();
      const userRoot = makeTempDir();
      dirs.push(systemDir, userRoot);

      writeSkill(systemDir, 'tdd.md', SKILL_A);
      const catalog = new SkillCatalog(systemDir, userRoot);

      // Populate cache for two users
      const user1Skills = catalog.getForUser('user1');
      const user2Skills = catalog.getForUser('user2');
      expect(user1Skills).toHaveLength(1);
      expect(user2Skills).toHaveLength(1);

      // Add a new system skill
      writeSkill(systemDir, 'code-review.md', SKILL_B);

      // Before refresh: both users still see cached 1 skill
      expect(catalog.getForUser('user1')).toHaveLength(1);
      expect(catalog.getForUser('user2')).toHaveLength(1);

      // After refresh: all caches cleared, system re-scanned
      catalog.refreshSystem();
      expect(catalog.getForUser('user1')).toHaveLength(2);
      expect(catalog.getForUser('user2')).toHaveLength(2);
    });
  });

  describe('getSystemDir / getUserSkillDir', () => {
    it('returns the configured directories', () => {
      const systemDir = makeTempDir();
      const userRoot = makeTempDir();
      dirs.push(systemDir, userRoot);

      const catalog = new SkillCatalog(systemDir, userRoot);
      expect(catalog.getSystemDir()).toBe(systemDir);
      expect(catalog.getUserSkillDir('user1')).toBe(path.join(userRoot, 'user1', 'skills'));
    });
  });
});
