import { createRef } from 'react';
import { render, screen, act, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import MediaUploader from './media-uploader';
import { xhrUpload } from '../../../../lib/xhr-upload';
import createPdf from '../../../../utils/createPdf';
import readFiles, { type ExtendedFile } from '../../../../lib/read-files';

// ファイル名から連番 id を生成する（重複削除の検証で安定させるため）
const { randomMock } = vi.hoisted(() => {
  let counter = 0;
  return {
    randomMock: () => {
      counter += 1;
      return `id-${counter}`;
    },
  };
});

vi.mock('../../../../lib/xhr-upload', () => ({ xhrUpload: vi.fn() }));
vi.mock('../../../../utils/createPdf', () => ({ default: vi.fn() }));
vi.mock('../../../../lib/read-files', () => ({ default: vi.fn() }));
vi.mock('../../../../lib/resize-image/util', () => ({
  // new される側のモックは class で定義する（アロー関数だと "is not a constructor" になる）
  default: class {
    getBlobFromFile = vi.fn().mockResolvedValue({ blob: new Blob(['resized'], { type: 'image/jpeg' }), resize: true });
  },
}));
vi.mock('../../../../utils', () => ({
  random: randomMock,
  getExt: (name: string) => name.split('.').pop() ?? '',
  dataURItoBlob: () => new Blob(['thumb'], { type: 'image/png' }),
}));
vi.mock('../../../../components/drop-zone/drop-zone', () => ({
  default: () => <div data-testid="dropzone" />,
}));
vi.mock('../../../../components/progress-bar/progress-bar', () => ({
  default: ({ value, variant }: { value: number | null; variant?: string }) => (
    <div data-testid="progress-bar" data-value={String(value)} data-variant={variant} />
  ),
}));
vi.mock('../../../../components/rich-select/creatable-select', () => ({
  // render 時に props のコールバック（ラベル生成・バリデーション）を実行して挙動を通す
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  default: ({ formatCreateLabel, isValidNewOption, noOptionsMessage }: any) => {
    formatCreateLabel?.('new-tag');
    isValidNewOption?.('new-tag');
    isValidNewOption?.('   ');
    noOptionsMessage?.();
    return <div data-testid="creatable-select" />;
  },
}));

const imageExtendedFile = (name = 'image.png', type = 'image/png'): ExtendedFile => ({
  filetype: 'image',
  preview: 'data:image/png;base64,abc',
  blob: new Blob(['image'], { type }),
  file: new File(['image'], name, { type }),
});

const plainExtendedFile = (name = 'doc.pdf', type = 'application/pdf'): ExtendedFile => ({
  filetype: 'file',
  file: new File(['file'], name, { type }),
});

const renderUploader = (props: Record<string, unknown> = {}) => {
  const actions = { setItem: vi.fn(), fetchMediaList: vi.fn() };
  const ref = createRef<MediaUploader>();
  const view = render(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    <MediaUploader ref={ref} largeSize={1200} config={{} as any} actions={actions as any} {...props} />
  );
  return { view, ref, actions, instance: () => ref.current as MediaUploader };
};

let alertSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
  // jsdom の Blob/File には text() が未実装のことがあるため補う（SVG のサニタイズ経路で使用）
  if (typeof Blob.prototype.text !== 'function') {
    // eslint-disable-next-line no-extend-native, func-names
    Blob.prototype.text = function () {
      return Promise.resolve('');
    };
  }
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const w = window as any;
  w.ACMS = w.ACMS || {};
  w.ACMS.i18n = vi.fn((key: string, opts?: Record<string, unknown>) => (opts ? `${key}:${JSON.stringify(opts)}` : key));
  w.ACMS.i18n.lng = 'ja';
  w.ACMS.Config = {
    lgImg: 'normal:1200',
    mediaClientResize: 'on',
    root: '/',
  };
  w.csrfToken = 'test-token';
  vi.mocked(readFiles).mockResolvedValue([]);
  vi.mocked(xhrUpload).mockResolvedValue({ data: { media_id: 1, media_thumbnail: 't.png' } } as never);
  vi.mocked(createPdf).mockResolvedValue('data:image/png;base64,thumb');
  alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
  vi.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
  vi.clearAllMocks();
});

describe('MediaUploader / レンダリング', () => {
  it('ステージにメディアが無いとき案内文を表示し、アップロードボタンは出さない', () => {
    renderUploader();
    expect(screen.getByText('media.no_staged_media')).toBeInTheDocument();
    expect(screen.getByTestId('dropzone')).toBeInTheDocument();
    expect(screen.getByTestId('creatable-select')).toBeInTheDocument();
    expect(screen.queryByText('media.upload')).not.toBeInTheDocument();
  });

  it('showUploadButton が false のときはステージがあってもアップロードボタンを出さない', () => {
    const { instance } = renderUploader({ showUploadButton: false });
    act(() => {
      instance().onComplete([imageExtendedFile()]);
    });
    expect(screen.queryByText('media.upload')).not.toBeInTheDocument();
  });
});

describe('MediaUploader / uploadLimit・uploadConcurrency getter', () => {
  it('config 未設定時はデフォルト（100 / 4）を返す', () => {
    const { instance } = renderUploader();
    expect(instance().uploadLimit).toBe(100);
    expect(instance().uploadConcurrency).toBe(4);
  });

  it('config に正の数値が設定されていればその値を返す', () => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (window as any).ACMS.Config.mediaUploadLimit = '50';
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (window as any).ACMS.Config.mediaUploadConcurrency = 6;
    const { instance } = renderUploader();
    expect(instance().uploadLimit).toBe(50);
    expect(instance().uploadConcurrency).toBe(6);
  });

  it('0・負数・非数値はデフォルトにフォールバックする', () => {
    const { instance } = renderUploader();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const config = (window as any).ACMS.Config;
    config.mediaUploadLimit = '0';
    expect(instance().uploadLimit).toBe(100);
    config.mediaUploadLimit = '-3';
    expect(instance().uploadLimit).toBe(100);
    config.mediaUploadConcurrency = 'abc';
    expect(instance().uploadConcurrency).toBe(4);
  });
});

describe('MediaUploader / onComplete', () => {
  it('ファイルを追加するとステージに反映され、案内文が消える', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('photo.png')]);
    });
    expect(screen.getByText('photo.png')).toBeInTheDocument();
    expect(screen.queryByText('media.no_staged_media')).not.toBeInTheDocument();
    expect(screen.getByText('media.upload')).toBeInTheDocument();
  });

  it('同名ファイルは重複として1件にまとめられる', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('same.png'), imageExtendedFile('same.png')]);
    });
    expect(instance().state.items).toHaveLength(1);
  });

  it('同名でも中身（サイズ・更新日時）が異なるファイルは別ファイルとして扱われる', () => {
    const { instance } = renderUploader();
    const fileA: ExtendedFile = {
      filetype: 'image',
      preview: 'data:image/png;base64,abc',
      blob: new Blob(['image-a'], { type: 'image/png' }),
      file: new File(['image-a'], 'same.png', { type: 'image/png', lastModified: 1000 }),
    };
    const fileB: ExtendedFile = {
      filetype: 'image',
      preview: 'data:image/png;base64,abc',
      blob: new Blob(['image-b-longer'], { type: 'image/png' }),
      file: new File(['image-b-longer'], 'same.png', { type: 'image/png', lastModified: 2000 }),
    };
    act(() => {
      instance().onComplete([fileA, fileB]);
    });
    expect(instance().state.items).toHaveLength(2);
  });

  it('上限を超えるとアラートを出し、上限件数に切り詰める', () => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (window as any).ACMS.Config.mediaUploadLimit = '2';
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('a.png'), imageExtendedFile('b.png'), imageExtendedFile('c.png')]);
    });
    expect(alertSpy).toHaveBeenCalledWith('media.more_than_limit_not_allowed:{"limit":2}');
    expect(instance().state.items).toHaveLength(2);
  });

  it('既にアップロード済みのアイテムは再追加時に除外される', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('keep.png')]);
    });
    act(() => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      instance().setUploadedItem(instance().state.items[0], { media_id: 9 } as any);
    });
    act(() => {
      instance().onComplete([plainExtendedFile('new.pdf')]);
    });
    const names = instance().state.items.map((item) => item.name);
    expect(names).toContain('new.pdf');
    expect(names).not.toContain('keep.png');
  });

  it('非画像ファイルはファイルアイコンで表示される', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([plainExtendedFile('document.pdf')]);
    });
    expect(screen.getByText('document.pdf')).toBeInTheDocument();
    expect(document.querySelector('.acms-admin-media-upload-file')).toBeInTheDocument();
  });
});

describe('MediaUploader / componentDidMount', () => {
  it('props.files があれば readFiles を経由してステージに展開する', async () => {
    vi.mocked(readFiles).mockResolvedValue([imageExtendedFile('mounted.png')]);
    renderUploader({ files: [new File(['x'], 'mounted.png', { type: 'image/png' })] });
    await waitFor(() => {
      expect(screen.getByText('mounted.png')).toBeInTheDocument();
    });
  });
});

describe('MediaUploader / upload', () => {
  it('成功するとレスポンスデータを解決し、進捗を表示する', async () => {
    vi.mocked(xhrUpload).mockImplementation((_url, _fd, options) => {
      options?.onUploadProgress?.({ loaded: 5, total: 10 });
      return Promise.resolve({ data: { media_id: 7 } } as never);
    });
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('ok.png')]);
    });
    const item = instance().state.items[0];
    const result = await act(async () => instance().upload(item.file, 'tag1', 'ok.png', item));
    expect(result).toEqual({ media_id: 7 });
    expect(xhrUpload).toHaveBeenCalledTimes(1);
  });

  it('PDF はサムネイルを生成してから送信する', async () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([plainExtendedFile('report.pdf')]);
    });
    const item = instance().state.items[0];
    await act(async () => instance().upload(item.file, '', 'report.pdf', item));
    expect(createPdf).toHaveBeenCalledTimes(1);
  });

  it('レスポンスが failure のときはアラートを出す', async () => {
    vi.mocked(xhrUpload).mockResolvedValue({ data: { status: 'failure', message: '保存に失敗しました' } } as never);
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('ng.png')]);
    });
    const item = instance().state.items[0];
    await act(async () => {
      await expect(instance().upload(item.file, '', 'ng.png', item)).rejects.toThrow('保存に失敗しました');
    });
    expect(alertSpy).toHaveBeenCalledWith('保存に失敗しました');
  });

  it('レスポンスにデータが無いときは不明エラーのアラートを出す', async () => {
    vi.mocked(xhrUpload).mockResolvedValue(null as never);
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('empty.png')]);
    });
    const item = instance().state.items[0];
    await act(async () => {
      await expect(instance().upload(item.file, '', 'empty.png', item)).rejects.toThrow('media.error.unknown');
    });
    expect(alertSpy).toHaveBeenCalledWith('media.error.unknown');
  });
});

describe('MediaUploader / uploadItems の種別ごとの分岐', () => {
  const uploadSingle = async (file: ExtendedFile) => {
    const { instance, actions } = renderUploader();
    act(() => {
      instance().onComplete([file]);
    });
    await act(async () => {
      await instance().uploadItems();
    });
    return { instance, actions };
  };

  it('通常画像はリサイズしてアップロードする', async () => {
    const { instance, actions } = await uploadSingle(imageExtendedFile('normal.png', 'image/png'));
    expect(xhrUpload).toHaveBeenCalledTimes(1);
    expect(actions.fetchMediaList).toHaveBeenCalledTimes(1);
    expect(instance().state.items[0].uploadedItem).toBeDefined();
    expect(instance().state.loading).toBe(false);
  });

  it('SVG はサニタイズしてアップロードする', async () => {
    const svg: ExtendedFile = {
      filetype: 'image',
      preview: 'data:image/svg+xml;base64,abc',
      blob: new Blob(['<svg></svg>'], { type: 'image/svg+xml' }),
      file: new File(['<svg onload="alert(1)"></svg>'], 'icon.svg', { type: 'image/svg+xml' }),
    };
    await uploadSingle(svg);
    expect(xhrUpload).toHaveBeenCalledTimes(1);
  });

  it('GIF はリサイズせずアップロードする', async () => {
    await uploadSingle(imageExtendedFile('anim.gif', 'image/gif'));
    expect(xhrUpload).toHaveBeenCalledTimes(1);
  });

  it('HEIC は blob をアップロードする', async () => {
    await uploadSingle(imageExtendedFile('photo.heic', 'image/heic'));
    expect(xhrUpload).toHaveBeenCalledTimes(1);
  });

  it('非画像ファイルはそのままアップロードする', async () => {
    await uploadSingle(plainExtendedFile('data.pdf'));
    expect(xhrUpload).toHaveBeenCalledTimes(1);
  });

  it('複数ファイルをまとめてアップロードできる', async () => {
    const { instance, actions } = renderUploader();
    act(() => {
      instance().onComplete([
        imageExtendedFile('1.png'),
        imageExtendedFile('2.gif', 'image/gif'),
        plainExtendedFile('3.pdf'),
      ]);
    });
    await act(async () => {
      await instance().uploadItems();
    });
    expect(xhrUpload).toHaveBeenCalledTimes(3);
    expect(actions.fetchMediaList).toHaveBeenCalledTimes(1);
  });

  it('一部のアップロードが失敗しても残りを処理し、ローディングを解除する', async () => {
    let call = 0;
    vi.mocked(xhrUpload).mockImplementation(() => {
      call += 1;
      if (call === 2) {
        return Promise.resolve({ data: { status: 'failure', message: 'NG' } } as never);
      }
      return Promise.resolve({ data: { media_id: call } } as never);
    });
    const { instance, actions } = renderUploader();
    act(() => {
      instance().onComplete([
        imageExtendedFile('a.gif', 'image/gif'),
        imageExtendedFile('b.gif', 'image/gif'),
        imageExtendedFile('c.gif', 'image/gif'),
      ]);
    });
    await act(async () => {
      await instance().uploadItems();
    });
    // 失敗したタスクで worker が停止せず、全件が試行されローディングも解除される
    expect(xhrUpload).toHaveBeenCalledTimes(3);
    expect(instance().state.loading).toBe(false);
    expect(actions.fetchMediaList).toHaveBeenCalledTimes(1);
  });

  it('複数ファイルを並列アップロードしても全アイテムの完了状態が反映される', async () => {
    vi.mocked(xhrUpload).mockImplementation(() => Promise.resolve({ data: { media_id: 1 } } as never));
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([
        imageExtendedFile('1.gif', 'image/gif'),
        imageExtendedFile('2.gif', 'image/gif'),
        imageExtendedFile('3.gif', 'image/gif'),
        imageExtendedFile('4.gif', 'image/gif'),
        imageExtendedFile('5.gif', 'image/gif'),
      ]);
    });
    await act(async () => {
      await instance().uploadItems();
    });
    // 状態更新の競合で取り残されるアイテムが無く、全件が完了状態（uploadedItem セット・progress=-1）になる
    expect(instance().state.items).toHaveLength(5);
    expect(instance().state.items.every((item) => item.uploadedItem !== undefined)).toBe(true);
    expect(instance().state.items.every((item) => item.progress === -1)).toBe(true);
  });
});

describe('MediaUploader / アイテム操作', () => {
  it('removeFile で対象アイテムを取り除く', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('x.png'), imageExtendedFile('y.png')]);
    });
    const target = instance().state.items[0];
    act(() => {
      instance().removeFile(target);
    });
    expect(instance().state.items).toHaveLength(1);
    expect(instance().state.items[0].name).toBe('y.png');
  });

  it('editItem はアップロード済みアイテムを編集対象に渡す', () => {
    const { instance, actions } = renderUploader();
    const uploaded = { media_id: 3 };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    instance().editItem({ uploadedItem: uploaded } as any);
    expect(actions.setItem).toHaveBeenCalledWith(uploaded);
  });

  it('editItem は未アップロードのアイテムでは何もしない', () => {
    const { instance, actions } = renderUploader();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    instance().editItem({} as any);
    expect(actions.setItem).not.toHaveBeenCalled();
  });

  it('removeUploadedItems はアップロード済みのみ取り除く', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('done.png'), imageExtendedFile('staged.png')]);
    });
    act(() => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      instance().setUploadedItem(instance().state.items[0], { media_id: 1 } as any);
    });
    act(() => {
      instance().removeUploadedItems();
    });
    expect(instance().state.items).toHaveLength(1);
    expect(instance().state.items[0].name).toBe('staged.png');
  });

  it('setProgressBar で進捗を更新するとプログレスバーが描画される', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('p.png')]);
    });
    act(() => {
      instance().setProgressBar(instance().state.items[0], 40);
    });
    // 個別バーは loading-bar インライン。画像に重ねる薄いバーの幅で進捗を確認する
    const bar = document.querySelector('.acms-admin-loading-bar-inner') as HTMLElement;
    expect(bar).not.toBeNull();
    expect(bar.style.width).toBe('40%');
  });

  it('進捗エラーのアイテムの個別バーは -alert 表示になる', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('e.png')]);
    });
    act(() => {
      instance().setProgressBar(instance().state.items[0], 100, true);
    });
    const bar = document.querySelector('.acms-admin-loading-bar-inner') as HTMLElement;
    expect(bar.classList.contains('-alert')).toBe(true);
  });

  it('存在しないアイテムへの状態更新は無視される', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('a.png')]);
    });
    act(() => {
      // 一致する id / idx が無いアイテムを渡すと index === -1 で更新がスキップされる
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      instance().setProgressBar({ id: 'missing', idx: 999 } as any, 50);
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      instance().setUploadedItem({ idx: 999 } as any, { media_id: 1 } as any);
    });
    expect(instance().state.items).toHaveLength(1);
    expect(instance().state.items[0].progress).toBe(0);
    expect(instance().state.items[0].uploadedItem).toBeUndefined();
  });

  it('アップロード済みアイテムには編集ボタンが表示され、押すと編集対象に渡る', () => {
    const { instance, actions } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('edit.png')]);
    });
    act(() => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      instance().setUploadedItem(instance().state.items[0], { media_id: 5 } as any);
    });
    const editButton = screen.getByText('media.edit');
    act(() => {
      editButton.click();
    });
    expect(actions.setItem).toHaveBeenCalledWith({ media_id: 5 });
  });
});

describe('MediaUploader / 全体進捗と離脱警告', () => {
  it('uploadProgress は完了件数・総件数・全体パーセントを返す', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('a.gif', 'image/gif'), imageExtendedFile('b.gif', 'image/gif')]);
    });
    act(() => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      instance().setUploadedItem(instance().state.items[0], { media_id: 1 } as any);
      instance().setProgressBar(instance().state.items[1], 50);
    });
    const progress = instance().uploadProgress;
    expect(progress.total).toBe(2);
    expect(progress.done).toBe(1);
    expect(progress.percent).toBe(75); // (100 + 50) / 2
  });

  it('アイテムが無いとき uploadProgress は 0 を返す', () => {
    const { instance } = renderUploader();
    expect(instance().uploadProgress).toEqual({ done: 0, total: 0, percent: 0, errorCount: 0 });
  });

  it('アップロード開始後は全体進捗バーが表示される', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('a.gif', 'image/gif')]);
    });
    act(() => {
      instance().setState({ hasUploadedItems: true });
    });
    expect(screen.getByText(/upload_overall_progress/)).toBeInTheDocument();
  });

  it('全件完了すると全体バーが完了表示（upload_all_complete）に変わる', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('a.gif', 'image/gif')]);
    });
    act(() => {
      instance().setState({ hasUploadedItems: true });
    });
    act(() => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      instance().setUploadedItem(instance().state.items[0], { media_id: 1 } as any);
    });
    expect(screen.getByText(/upload_all_complete/)).toBeInTheDocument();
    expect(screen.queryByText(/upload_overall_progress/)).not.toBeInTheDocument();
  });

  it('一部失敗で全件処理し終えると完了件数と失敗件数（danger）を表示する', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().onComplete([imageExtendedFile('a.gif', 'image/gif'), imageExtendedFile('b.gif', 'image/gif')]);
    });
    act(() => {
      instance().setState({ hasUploadedItems: true });
      // 1件成功・1件失敗（progressError）で全件処理済みにする
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      instance().setUploadedItem(instance().state.items[0], { media_id: 1 } as any);
      instance().setProgressBar(instance().state.items[1], 100, true);
    });
    const overallText = document.querySelector('.acms-admin-media-upload-overall-text') as HTMLElement;
    expect(overallText.textContent).toContain('upload_done_count');
    expect(overallText.textContent).toContain('upload_failed_count');
    // 失敗件数は danger で強調される
    expect(overallText.querySelector('.acms-admin-text-danger')).not.toBeNull();
    // エラーなし版の完了テキストは出ない
    expect(screen.queryByText(/upload_all_complete/)).not.toBeInTheDocument();
  });

  it('アップロード中（loading）はページ離脱で確認ダイアログを出す', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().setState({ loading: true });
    });
    const event = { preventDefault: vi.fn(), returnValue: undefined } as unknown as BeforeUnloadEvent;
    instance().handleBeforeUnload(event);
    expect(event.preventDefault).toHaveBeenCalled();
    expect(event.returnValue).toBe('');
  });

  it('アップロード中でなければ離脱警告を出さない', () => {
    const { instance } = renderUploader();
    const event = { preventDefault: vi.fn(), returnValue: undefined } as unknown as BeforeUnloadEvent;
    instance().handleBeforeUnload(event);
    expect(event.preventDefault).not.toHaveBeenCalled();
  });
});

describe('MediaUploader / タグ', () => {
  it('addTags で選択したタグがカンマ区切りの label になる', () => {
    const { instance } = renderUploader();
    act(() => {
      instance().addTags([
        { value: 'tagA', label: 'tagA' },
        { value: 'tagB', label: 'tagB' },
      ]);
    });
    expect(instance().state.label).toBe('tagA,tagB');
  });

  it('makeTags は label を option 配列に変換する', () => {
    const { instance } = renderUploader();
    expect(instance().makeTags('one,two')).toEqual([
      { value: 'one', label: 'one' },
      { value: 'two', label: 'two' },
    ]);
  });

  it('makeTags は空文字なら null を返す', () => {
    const { instance } = renderUploader();
    expect(instance().makeTags('')).toBeNull();
  });
});
