import { describe, expect, it, vi, afterEach, beforeAll } from 'vitest';
import { waitFor } from '@testing-library/react';
import type { BlockDefinition } from '@ablogcms/block-editor/types';
import { registerAcmsElements } from './register';
import { getBlockDefinitions } from '../features/block-editor/block-definitions';

vi.mock('@ablogcms/block-editor/tiptap', () => ({ tiptap: {} }));
vi.mock('../features/block-editor/block-definitions', () => ({
  getBlockDefinitions: vi.fn(() => ({})),
}));

const dummyDefinition = (label: string, menuCategory?: string): BlockDefinition => ({
  label,
  description: label,
  icon: 'title',
  isTextMenu: false,
  aliases: [],
  menuCategory,
  createActions: () => ({ action: () => {}, convert: () => {}, isDisabled: () => false, isActive: () => false }),
});

const getSelect = () => document.querySelector('select');
const getHiddenInput = () => document.querySelector<HTMLInputElement>('input[type="hidden"]');

const createElement = ({ name = 'block_editor_block_menu_type[]', value = '' } = {}) => {
  const el = document.createElement('acms-block-editor-menu-type-select');
  const hiddenInput = document.createElement('input');
  hiddenInput.type = 'hidden';
  hiddenInput.name = name;
  hiddenInput.value = value;
  el.append(hiddenInput);
  return el;
};

describe('acms-block-editor-menu-type-select', () => {
  beforeAll(() => {
    registerAcmsElements();
  });

  afterEach(() => {
    document.body.innerHTML = '';
    vi.clearAllMocks();
  });

  it('子孫の hidden input が無ければ console.error に報告し、例外は投げない', async () => {
    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    const el = document.createElement('acms-block-editor-menu-type-select');

    expect(() => document.body.appendChild(el)).not.toThrow();
    expect(consoleErrorSpy).toHaveBeenCalled();
    expect(getBlockDefinitions).not.toHaveBeenCalled();

    consoleErrorSpy.mockRestore();
  });

  it('コアの BLOCK_DEFINITIONS から select/optgroup/option をすべてレンダリングする', async () => {
    const el = createElement();
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getSelect()).not.toBeNull();
    });

    const select = getSelect()!;
    // select は編集用 UI 専用で、実際にサーバーへ送信する name は hidden input 側が持つ
    expect(select.hasAttribute('name')).toBe(false);
    expect(getHiddenInput()?.name).toBe('block_editor_block_menu_type[]');
    // コアの基本グループ（paragraph 等）が optgroup として存在する
    const basicGroup = select.querySelector('optgroup[label="基本"]');
    expect(basicGroup).not.toBeNull();
    expect(basicGroup?.querySelector('option[value="paragraph"]')?.textContent).toBe('段落');
    // レイアウトグループも存在する
    expect(
      select.querySelector('optgroup[label="レイアウト"]')?.querySelector('option[value="columns1"]')
    ).not.toBeNull();
  });

  it('host の root-class 属性が実際にレンダリングされる select へ転送される', async () => {
    const el = createElement();
    el.setAttribute('root-class', 'my-custom-class');
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getSelect()).not.toBeNull();
    });

    expect(getSelect()?.className).toBe('my-custom-class');
    // root-class は class と違い host 自身の見た目に意味を持つ標準属性ではないため取り除かない。
    expect(el.getAttribute('root-class')).toBe('my-custom-class');
  });

  it('プラグイン提供の BlockDefinition を menuCategory ごとに振り分ける（未指定は「プラグイン」）', async () => {
    vi.mocked(getBlockDefinitions).mockReturnValue({
      callout: dummyDefinition('コールアウト'),
      customHeading: dummyDefinition('カスタム見出し', 'basic'),
    });
    const el = createElement();
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getSelect()?.querySelector('option[value="callout"]')).not.toBeNull();
    });

    const select = getSelect()!;
    // menuCategory 未指定のプラグイン提供 type は「プラグイン」optgroup にまとめる
    const pluginGroup = select.querySelector('optgroup[label="プラグイン"]');
    expect(pluginGroup?.querySelector('option[value="callout"]')?.textContent).toBe('コールアウト');
    // menuCategory を明示指定すればコアと同じ optgroup に混ざる
    const basicGroup = select.querySelector('optgroup[label="基本"]');
    expect(basicGroup?.querySelector('option[value="customHeading"]')?.textContent).toBe('カスタム見出し');
  });

  it('hidden input の value が示す type を選択状態にする', async () => {
    const el = createElement({ value: 'heading2' });
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getSelect()?.value).toBe('heading2');
    });
  });

  it('hidden input の value がコア・プラグインどちらのマスターにも無い場合は先頭が選択され、hidden input へ同期される', async () => {
    const el = createElement({ value: 'unknown-type' });
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getSelect()).not.toBeNull();
    });
    // ブラウザの既定挙動（一致する option が無ければ先頭が選択される）に委ねる
    expect(getSelect()?.value).not.toBe('unknown-type');
    // 実際に選択されている値（先頭 option）を hidden input 側へ同期する
    expect(getHiddenInput()?.value).toBe(getSelect()?.value);
  });

  it('select の選択を変更すると hidden input の value に反映される', async () => {
    const el = createElement({ value: 'paragraph' });
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getSelect()?.value).toBe('paragraph');
    });

    const select = getSelect()!;
    select.value = 'bulletList';
    select.dispatchEvent(new Event('change'));

    expect(getHiddenInput()?.value).toBe('bulletList');
  });

  it('複製されて再度 connectedCallback が呼ばれても select を二重に描画しない', async () => {
    const el = createElement();
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getSelect()).not.toBeNull();
    });

    el.remove();
    document.body.appendChild(el);

    await waitFor(() => {
      expect(document.querySelectorAll('select')).toHaveLength(1);
    });
  });

  it('並べ替え（DOM 移動による再接続）では、未保存の選択が hidden input の初期値に巻き戻らない', async () => {
    const el = createElement({ value: 'paragraph' });
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getSelect()).not.toBeNull();
    });

    // 管理者が保存前に選択を変更する
    const select = getSelect()!;
    select.value = 'bulletList';
    select.dispatchEvent(new Event('change'));

    // jQuery UI sortable と同じ「実際に DOM から取り除いて挿し直す」再接続をシミュレートする
    el.remove();
    document.body.appendChild(el);

    await waitFor(() => {
      expect(document.querySelectorAll('select')).toHaveLength(1);
    });
    expect(getSelect()?.value).toBe('bulletList');
    expect(getHiddenInput()?.value).toBe('bulletList');
  });
});
