import { describe, it, expect, vi } from 'vitest';
import { buildCommands, filterCommands, groupCommands, shouldOpenForKeyEvent, type CommandContext } from './command-palette';

function ctx(over: Partial<CommandContext> = {}): CommandContext {
  return {
    navigatePage: vi.fn(), openTask: vi.fn(), setTheme: vi.fn(),
    navItems: [{ id: 'tasks', label: 'タスク' }, { id: 'settings', label: '設定' }],
    tasks: [{ id: 407, title: 'PR 407 fix' }, { id: 12, title: 'UI resize' }],
    ...over,
  };
}

describe('buildCommands', () => {
  it('emits nav + 3 theme + task commands with correct ids/keywords', () => {
    const items = buildCommands(ctx());
    expect(items.filter(i => i.id.startsWith('nav:')).length).toBe(2);
    expect(items.filter(i => i.id.startsWith('theme:')).length).toBe(3);
    const task = items.find(i => i.id === 'task:407')!;
    expect(task.group).toBe('task');
    expect(task.keywords).toBe('#407');
    expect(task.hint).toBe('#407');
  });
  it('run() invokes the right context fn', () => {
    const c = ctx();
    const items = buildCommands(c);
    items.find(i => i.id === 'nav:settings')!.run();
    expect(c.navigatePage).toHaveBeenCalledWith('settings');
    items.find(i => i.id === 'theme:dark')!.run();
    expect(c.setTheme).toHaveBeenCalledWith('dark');
    items.find(i => i.id === 'task:407')!.run();
    expect(c.openTask).toHaveBeenCalledWith(407);
  });
});

describe('filterCommands', () => {
  it('empty query: all nav/theme + first 5 tasks', () => {
    const many = ctx({ tasks: Array.from({ length: 9 }, (_, i) => ({ id: i + 1, title: `t${i + 1}` })) });
    const out = filterCommands(buildCommands(many), '');
    expect(out.filter(i => i.group === 'task').length).toBe(5);
    expect(out.filter(i => i.group === 'nav').length).toBe(2 + 3);
  });
  it('matches label and #id, prefix ranks first', () => {
    const out = filterCommands(buildCommands(ctx()), '設定');
    expect(out[0].id).toBe('nav:settings');
    const byId = filterCommands(buildCommands(ctx()), '#407');
    expect(byId.some(i => i.id === 'task:407')).toBe(true);
  });
  it('no match → empty', () => {
    expect(filterCommands(buildCommands(ctx()), 'zzzzz')).toEqual([]);
  });
});

describe('groupCommands', () => {
  it('orders nav before task and drops empty groups', () => {
    const groups = groupCommands(filterCommands(buildCommands(ctx()), ''));
    expect(groups.map(g => g.group)).toEqual(['nav', 'task']);
    const navOnly = groupCommands(filterCommands(buildCommands(ctx()), '設定'));
    expect(navOnly.map(g => g.group)).toEqual(['nav']);
  });
});

describe('shouldOpenForKeyEvent', () => {
  const base = { metaKey: false, ctrlKey: false, key: 'k', isComposing: false };
  it('opens on meta+k and ctrl+k', () => {
    expect(shouldOpenForKeyEvent({ ...base, metaKey: true }, { inTerminal: false, inEditor: false })).toBe(true);
    expect(shouldOpenForKeyEvent({ ...base, ctrlKey: true }, { inTerminal: false, inEditor: false })).toBe(true);
  });
  it('ignores IME composition and non-k and bare k', () => {
    expect(shouldOpenForKeyEvent({ ...base, metaKey: true, isComposing: true }, { inTerminal: false, inEditor: false })).toBe(false);
    expect(shouldOpenForKeyEvent({ ...base, metaKey: true, key: 'j' }, { inTerminal: false, inEditor: false })).toBe(false);
    expect(shouldOpenForKeyEvent({ ...base, key: 'k' }, { inTerminal: false, inEditor: false })).toBe(false);
  });
  it('does not steal ctrl+k from a focused terminal, but meta+k still works', () => {
    expect(shouldOpenForKeyEvent({ ...base, ctrlKey: true }, { inTerminal: true, inEditor: false })).toBe(false);
    expect(shouldOpenForKeyEvent({ ...base, metaKey: true }, { inTerminal: true, inEditor: false })).toBe(true);
  });
  it('never steals from a rich editor (Monaco/contenteditable): both combos blocked', () => {
    expect(shouldOpenForKeyEvent({ ...base, metaKey: true }, { inTerminal: false, inEditor: true })).toBe(false);
    expect(shouldOpenForKeyEvent({ ...base, ctrlKey: true }, { inTerminal: false, inEditor: true })).toBe(false);
  });
});
