import { describe, it, expect, vi } from 'vitest';
import BLOCK_DEFINITIONS from '../index';
import createMockEditor from './createMockEditor';
import type { CommandItem } from '../../../types';

const TYPES = Object.keys(BLOCK_DEFINITIONS);

/**
 * 各 block の createActions が返す action / convert / isDisabled / isActive が
 * Editor モックに対して例外なく動作することを横断的に確認する。
 * 個別 block の詳細挙動 (paragraph / heading factory) は別ファイルでテストする。
 */
describe.each(TYPES)('block: %s', (name) => {
  const def = BLOCK_DEFINITIONS[name];
  const buildCmd = (overrides: Partial<CommandItem> = {}): CommandItem => ({
    name,
    label: def.label,
    description: def.description,
    iconName: def.icon,
    isTextMenu: def.isTextMenu,
    isDisabled: () => false,
    ...overrides,
  });

  it('action は recordUsage を呼び、editor.chain で何らかのメソッドを呼ぶ', () => {
    const recordUsage = vi.fn();
    const mock = createMockEditor();
    const cmd = buildCmd();

    def.createActions(cmd, { recordUsage }).action(mock.editor);

    expect(recordUsage).toHaveBeenCalledWith(cmd);
    expect(mock.chainCalls.length).toBeGreaterThan(0);
  });

  it('convert は例外を投げない', () => {
    const mock = createMockEditor();
    expect(() => def.createActions(buildCmd(), { recordUsage: () => {} }).convert(mock.editor, 0)).not.toThrow();
  });

  it('isDisabled / isActive は boolean を返す (isActive=false の場合)', () => {
    const mock = createMockEditor();
    mock.isActive.mockReturnValue(false);
    const actions = def.createActions(buildCmd(), { recordUsage: () => {} });

    expect(typeof actions.isDisabled(mock.editor, 0)).toBe('boolean');
    expect(typeof actions.isActive(mock.editor)).toBe('boolean');
  });

  it('isDisabled / isActive は boolean を返す (isActive=true の場合)', () => {
    const mock = createMockEditor();
    mock.isActive.mockReturnValue(true);
    const actions = def.createActions(buildCmd(), { recordUsage: () => {} });

    expect(typeof actions.isDisabled(mock.editor, 0)).toBe('boolean');
    expect(typeof actions.isActive(mock.editor)).toBe('boolean');
  });

  it('cmd.class 付きでも全 lambda が例外なく動作する', () => {
    const mock = createMockEditor();
    const cmd = buildCmd({ class: 'sample-class' });
    const actions = def.createActions(cmd, { recordUsage: () => {} });

    expect(() => actions.action(mock.editor)).not.toThrow();
    expect(() => actions.convert(mock.editor, 0)).not.toThrow();
    expect(typeof actions.isDisabled(mock.editor, 0)).toBe('boolean');
    expect(typeof actions.isActive(mock.editor)).toBe('boolean');
  });

  it('cmd.class 付き + isActive=true (同 class) でも全 lambda 動作', () => {
    const mock = createMockEditor();
    mock.isActive.mockImplementation((_type: string, attrs?: { class?: string }) => attrs?.class === 'sample-class');
    const cmd = buildCmd({ class: 'sample-class' });
    const actions = def.createActions(cmd, { recordUsage: () => {} });

    expect(typeof actions.isDisabled(mock.editor, 0)).toBe('boolean');
    expect(typeof actions.isActive(mock.editor)).toBe('boolean');
  });

  it('cmd.class 付き + 別 class が active (isSame=false かつ同 type active) でも isDisabled は boolean', () => {
    const mock = createMockEditor();
    // 自分の class は active でないが、type 単独 / 別 class は active という状態を作る
    mock.isActive.mockImplementation((_type: string, attrs?: { class?: string }) => {
      if (attrs && attrs.class === 'cmd-class') return false;
      return true;
    });
    const cmd = buildCmd({ class: 'cmd-class' });
    const actions = def.createActions(cmd, { recordUsage: () => {} });

    expect(typeof actions.isDisabled(mock.editor, 0)).toBe('boolean');
  });
});

/**
 * bulletList / numberedList / blockquote / codeBlock / table は
 * action 内で「カーソル位置から depth を辿って自分の type を探す」分岐を持つ。
 * この経路に入った場合にも setBlockAttrs が呼ばれることを確認する。
 */
describe('カーソル位置探索を伴う action', () => {
  const cases: Array<{ type: string; nodeName: string }> = [
    { type: 'bulletList', nodeName: 'bulletList' },
    { type: 'numberedList', nodeName: 'orderedList' },
    { type: 'blockquote', nodeName: 'blockquote' },
    { type: 'codeBlock', nodeName: 'codeBlock' },
    { type: 'table', nodeName: 'table' },
  ];

  it.each(cases)('$type: action で探索後に setBlockAttrs(class) が呼ばれる', ({ type, nodeName }) => {
    const def = BLOCK_DEFINITIONS[type];
    const mock = createMockEditor();
    mock.setSelectionDepth(2);
    mock.setNodeAt(2, nodeName);
    const cmd: CommandItem = {
      name: type,
      label: def.label,
      description: def.description,
      iconName: def.icon,
      isTextMenu: def.isTextMenu,
      isDisabled: () => false,
      class: 'q-style',
    };

    def.createActions(cmd, { recordUsage: () => {} }).action(mock.editor);

    expect(mock.getChainCallArgs('setBlockAttrs')).toEqual([0, { class: 'q-style' }]);
  });
});

/**
 * image / file / linkButton / table の convert は NodeSelection の判定があり、
 * isActive が true のとき updateAttributes が呼ばれる経路を持つ。
 */
describe('NodeSelection 経路の convert', () => {
  const cases: Array<{ type: string; nodeType: string }> = [
    { type: 'image', nodeType: 'imageBlock' },
    { type: 'file', nodeType: 'fileBlock' },
    { type: 'linkButton', nodeType: 'linkButton' },
    { type: 'table', nodeType: 'table' },
  ];

  it.each(cases)('$type: isActive=true 時に updateAttributes が呼ばれる', ({ type, nodeType }) => {
    const def = BLOCK_DEFINITIONS[type];
    const mock = createMockEditor();
    mock.isActive.mockImplementation((name: string) => name === nodeType);
    const cmd: CommandItem = {
      name: type,
      label: def.label,
      description: def.description,
      iconName: def.icon,
      isTextMenu: def.isTextMenu,
      isDisabled: () => false,
      class: 'foo',
    };

    def.createActions(cmd, { recordUsage: () => {} }).convert(mock.editor, 0);

    expect(mock.getChainCallArgs('updateAttributes')).toEqual([nodeType, { class: 'foo' }]);
  });
});
