import { describe, it, expect, vi, afterEach, beforeAll, beforeEach } from 'vitest';
import { waitFor, fireEvent } from '@testing-library/react';
import { fetchClient } from '@ablogcms/fetch-client';
import { registerAcmsElements } from './register';
import type { UnitEditorElement } from './unit-editor-element';

vi.mock('@ablogcms/fetch-client', () => ({
  fetchClient: { get: vi.fn() },
}));

vi.mock('@ablogcms/unit-editor/components/unit-editor', () => ({
  default: ({
    defaultValue,
    onCreate,
    onContentChange,
    className,
  }: {
    defaultValue: unknown[];
    onCreate: (editor: unknown) => void;
    onContentChange: (data: unknown[]) => void;
    className?: string;
  }) => (
    <div data-testid="unit-editor-stub" className={className}>
      <span data-testid="default-value">{JSON.stringify(defaultValue)}</span>
      <button type="button" onClick={() => onCreate({ id: 'fake-editor' })}>
        create
      </button>
      <button type="button" onClick={() => onContentChange([{ id: '1', type: 'text', children: [] }])}>
        transaction
      </button>
    </div>
  ),
}));

const getStub = () => document.querySelector('[data-testid="unit-editor-stub"]');

const createElement = (attrs: Record<string, string> = {}): UnitEditorElement => {
  const el = document.createElement('acms-unit-editor') as UnitEditorElement;
  Object.entries(attrs).forEach(([key, value]) => el.setAttribute(key, value));
  return el;
};

describe('acms-unit-editor', () => {
  beforeAll(() => {
    registerAcmsElements();
  });

  beforeEach(() => {
    // UnitEditor 配下は BlockEditorSettingsProvider（コア側）でラップされ、ブロックエディター設定を
    // 取得する。ここでは値そのものは検証対象ではないため、既定値で解決させておく。
    vi.mocked(fetchClient.get).mockReset();
    vi.mocked(fetchClient.get).mockResolvedValue({
      data: {},
      status: 200,
      statusText: 'OK',
      headers: new Headers(),
    });
    (window as unknown as { ACMS: { Config: Record<string, unknown>; Library: unknown } }).ACMS = {
      Config: { bid: 1, cid: 2 },
      Library: { acmsLink: vi.fn(() => 'ajax/edit/block-editor/settings.json') },
    };
  });

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

  it('default-value を渡して UnitEditor を自身にマウントする', async () => {
    const el = createElement({ 'default-value': '[{"id":"1","type":"text","children":[]}]' });
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getStub()).not.toBeNull();
    });
    expect(el.querySelector('[data-testid="default-value"]')?.textContent).toBe(
      JSON.stringify([{ id: '1', type: 'text', children: [] }])
    );
  });

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

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

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

  it('editor は接続前は null、editor 生成後にインスタンスを返す', async () => {
    const el = createElement();
    expect(el.editor).toBeNull();
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getStub()).not.toBeNull();
    });
    fireEvent.click(document.querySelector('button')!);

    expect(el.editor).toEqual({ id: 'fake-editor' });
  });

  it('ユニットツリーの変更（transaction）で change イベントが発火する', async () => {
    const el = createElement();
    document.body.appendChild(el);
    const handleChange = vi.fn();
    el.addEventListener('change', handleChange);

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

    const buttons = el.querySelectorAll('button');
    fireEvent.click(buttons[1]);

    expect(handleChange).toHaveBeenCalledTimes(1);
  });

  it('disconnectedCallback でアンマウントし、再接続すると再マウントする', async () => {
    const el = createElement({ 'default-value': '[]' });
    document.body.appendChild(el);

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

    el.remove();
    // unmount はマイクロタスクへ遅延される（物理的な DOM 移動と区別するため。詳細は
    // disconnectedCallback のコメント参照）。
    await waitFor(() => {
      expect(getStub()).toBeNull();
    });

    document.body.appendChild(el);
    await waitFor(() => {
      expect(getStub()).not.toBeNull();
    });
  });

  // Why not: この要素がカスタムユニットの中に埋め込まれる等で物理的に DOM 移動されると、
  // disconnectedCallback → connectedCallback が同期的に（マイクロタスクが実行される前に）
  // 再発火する。マイクロタスクの中で this.isConnected を見て「移動で戻ってきた」場合は
  // 何もしないことで、既存の root を壊さず・二重マウントもしないことを特徴づける。
  it('マイクロタスクが実行される前に再接続された場合（物理的な DOM 移動）は unmount せず、二重マウントもしない', async () => {
    const el = createElement({ 'default-value': '[]' });
    const parent = document.createElement('div');
    document.body.appendChild(parent);
    parent.appendChild(el);

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

    parent.removeChild(el);
    parent.appendChild(el);

    await Promise.resolve();
    await Promise.resolve();

    expect(document.querySelectorAll('[data-testid="unit-editor-stub"]').length).toBe(1);
  });

  it('default-value が不正な JSON でも例外を投げずに console.error に留める', async () => {
    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    const el = createElement({ 'default-value': '{invalid' });

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