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

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

vi.mock('@ablogcms/unit-editor/components/inplace-editor', () => ({
  default: ({ defaultValue, unitId, className }: { defaultValue: unknown[]; unitId: string; className?: string }) => (
    <div data-testid="inplace-editor-stub" className={className}>
      <span data-testid="default-value">{JSON.stringify(defaultValue)}</span>
      <span data-testid="unit-id">{unitId}</span>
    </div>
  ),
}));

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

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

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

  beforeEach(() => {
    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 / unit-id を渡して InplaceEditor を自身にマウントする', async () => {
    const el = createElement({
      'default-value': '[{"id":"1","type":"text","children":[]}]',
      'unit-id': 'unit-1',
    });
    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: [] }])
    );
    expect(el.querySelector('[data-testid="unit-id"]')?.textContent).toBe('unit-1');
  });

  it('host の root-class 属性が実際にレンダリングされる InplaceEditor のルート要素へ転送される', async () => {
    const el = createElement({ 'unit-id': 'unit-1' });
    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('disconnectedCallback でアンマウントし、再接続すると再マウントする', async () => {
    const el = createElement({ 'default-value': '[]', 'unit-id': 'unit-1' });
    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': '[]', 'unit-id': 'unit-1' });
    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="inplace-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();
  });
});
