import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest';
import { waitFor, fireEvent } from '@testing-library/react';
import { registerAcmsElements } from './register';
import type { RelatedEntryEditorElement } from './related-entry-editor-element';
import type { RelatedEntryType } from '../features/related-entry/types';

vi.mock('@features/related-entry/components/related-entries/related-entries', () => ({
  default: ({
    entries,
    type,
    title,
    moduleId,
    ctx,
    maxItem,
    onChange,
    className,
  }: {
    entries: RelatedEntryType[];
    type: string;
    title: string;
    moduleId: string;
    ctx: string;
    maxItem: number;
    onChange: (entries: RelatedEntryType[]) => void;
    className?: string;
  }) => (
    <div
      data-testid="related-entries-stub"
      data-type={type}
      data-title={title}
      data-module-id={moduleId}
      data-ctx={ctx}
      data-max-item={maxItem}
      className={className}
    >
      <span data-testid="entries">{JSON.stringify(entries)}</span>
      <button
        type="button"
        onClick={() => onChange([{ id: 99, title: 'new', categoryName: 'cat', image: '', url: '' }])}
      >
        change
      </button>
    </div>
  ),
}));

const getStub = () => document.querySelector('[data-testid="related-entries-stub"]');
const sampleEntries: RelatedEntryType[] = [
  { id: 1, title: 'entry1', categoryName: 'cat1', image: 'img1', url: 'url1' },
];

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

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

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

  it('json-id が指す script の内容を entries として RelatedEntries をマウントする', async () => {
    const script = document.createElement('script');
    script.type = 'application/json';
    script.id = 'entries-json';
    script.textContent = JSON.stringify(sampleEntries);
    document.body.appendChild(script);

    const el = createElement({
      type: 'news',
      'data-title': '関連ニュース',
      'module-id': '1',
      ctx: 'index',
      'max-item': '5',
      'json-id': 'entries-json',
    });
    document.body.appendChild(el);

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

    const stub = getStub()!;
    expect(stub.getAttribute('data-type')).toBe('news');
    expect(stub.getAttribute('data-title')).toBe('関連ニュース');
    expect(stub.getAttribute('data-module-id')).toBe('1');
    expect(stub.getAttribute('data-ctx')).toBe('index');
    expect(stub.getAttribute('data-max-item')).toBe('5');
    expect(el.value).toEqual(sampleEntries);
  });

  it('host の root-class 属性が実際にレンダリングされる RelatedEntries のルート要素へ転送される', async () => {
    const script = document.createElement('script');
    script.type = 'application/json';
    script.id = 'entries-json';
    script.textContent = '[]';
    document.body.appendChild(script);

    const el = createElement({ 'json-id': 'entries-json' });
    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('json-id が無い場合は console.error に報告し、例外は投げない', async () => {
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
    const el = createElement();

    expect(() => document.body.appendChild(el)).not.toThrow();
    expect(consoleError).toHaveBeenCalled();
    expect(getStub()).toBeNull();

    consoleError.mockRestore();
  });

  it('el.value = ... で値（配列）が反映される', async () => {
    const script = document.createElement('script');
    script.type = 'application/json';
    script.id = 'entries-json';
    script.textContent = '[]';
    document.body.appendChild(script);

    const el = createElement({ 'json-id': 'entries-json' });
    document.body.appendChild(el);

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

    el.value = sampleEntries;

    expect(el.value).toEqual(sampleEntries);
    await waitFor(() => {
      expect(JSON.parse(document.querySelector('[data-testid="entries"]')?.textContent || '[]')).toEqual(sampleEntries);
    });
  });

  it('内部の変更で el.value が更新され、change イベントが発火する', async () => {
    const script = document.createElement('script');
    script.type = 'application/json';
    script.id = 'entries-json';
    script.textContent = '[]';
    document.body.appendChild(script);

    const el = createElement({ 'json-id': 'entries-json' });
    document.body.appendChild(el);
    const handleChange = vi.fn();
    el.addEventListener('change', handleChange);

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

    fireEvent.click(document.querySelector('button')!);

    expect(el.value).toEqual([{ id: 99, title: 'new', categoryName: 'cat', image: '', url: '' }]);
    expect(handleChange).toHaveBeenCalledTimes(1);
  });

  it('max-item を省略した場合は entries が空配列で初期化される', async () => {
    const script = document.createElement('script');
    script.type = 'application/json';
    script.id = 'entries-json';
    script.textContent = '[]';
    document.body.appendChild(script);

    const el = createElement({ 'json-id': 'entries-json' });
    document.body.appendChild(el);

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

    expect(el.value).toEqual([]);
  });

  it('切断すると React がアンマウントされ、再接続で再マウントされる', async () => {
    const script = document.createElement('script');
    script.type = 'application/json';
    script.id = 'entries-json';
    script.textContent = '[]';
    document.body.appendChild(script);

    const el = createElement({ 'json-id': 'entries-json' });
    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 script = document.createElement('script');
    script.type = 'application/json';
    script.id = 'entries-json';
    script.textContent = '[]';
    document.body.appendChild(script);

    const el = createElement({ 'json-id': 'entries-json' });
    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="related-entries-stub"]').length).toBe(1);
  });
});
