import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest';
import { waitFor, fireEvent } from '@testing-library/react';
import { registerAcmsElements } from './register';
import type { SubCategorySelectElement } from './sub-category-select-element';

vi.mock('@features/sub-category/components/sub-category-select/sub-category-select', () => ({
  default: ({
    defaultValue,
    onChange,
    mainCategoryId,
    isDisabled,
    className,
  }: {
    defaultValue: { label: string; value: string }[];
    onChange: (value: { label: string; value: string }[]) => void;
    mainCategoryId?: number;
    isDisabled?: boolean;
    className?: string;
  }) => (
    <div
      data-testid="sub-category-select-stub"
      data-main-category-id={mainCategoryId}
      data-disabled={String(!!isDisabled)}
      className={className}
    >
      <span data-testid="default-value">{JSON.stringify(defaultValue)}</span>
      <button type="button" onClick={() => onChange([{ label: 'new', value: 'new' }])}>
        change
      </button>
    </div>
  ),
}));

const getStub = () => document.querySelector('[data-testid="sub-category-select-stub"]');

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

describe('acms-sub-category-select', () => {
  beforeAll(() => {
    registerAcmsElements();
  });

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

  it('target 先の input へマウントし、既定の category-id ([name=category_id]) を参照する', async () => {
    document.body.innerHTML = `
      <input name="category_id" value="7" />
      <input id="hidden-sub-category" value="1,2" />
    `;
    const el = createElement({
      target: '#hidden-sub-category',
      'default-value': '1,2',
      'default-label': 'ラベル1,ラベル2',
    });
    document.body.appendChild(el);

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

    expect(getStub()?.getAttribute('data-main-category-id')).toBe('7');
    expect(JSON.parse(document.querySelector('[data-testid="default-value"]')?.textContent || '[]')).toEqual([
      { label: 'ラベル1', value: '1' },
      { label: 'ラベル2', value: '2' },
    ]);
  });

  // Why: target 属性を使わず、<acms-sub-category-select> 自身の子孫に hidden input を直接
  // 置くだけで動くようにする（block-editor と同じ「自己完結」の使い方を選べるようにするため）。
  it('target が無くても host 内の hidden input を検出してマウントする', async () => {
    document.body.innerHTML = `<input name="category_id" value="7" />`;
    const el = createElement({ 'default-value': '1', 'default-label': 'ラベル1' });
    el.innerHTML = `<input type="hidden" name="sub_category" value="" />`;
    const input = el.querySelector('input') as HTMLInputElement;
    document.body.appendChild(el);

    await waitFor(() => {
      expect(getStub()).not.toBeNull();
    });
    expect(getStub()?.getAttribute('data-main-category-id')).toBe('7');
    expect(input.isConnected).toBe(true);

    fireEvent.click(document.querySelector('button')!);
    expect(input.value).toBe('new');
  });

  it('host の root-class 属性が実際にレンダリングされる SubCategorySelect のルート要素へ転送される', async () => {
    document.body.innerHTML = `<input id="hidden-sub-category" value="" />`;
    const el = createElement({ target: '#hidden-sub-category' });
    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('category-id 属性でカテゴリー input のセレクタを上書きできる', async () => {
    document.body.innerHTML = `
      <input id="main-category" value="3" />
      <input id="hidden-sub-category" value="" />
    `;
    const el = createElement({ target: '#hidden-sub-category', 'category-id': '#main-category' });
    document.body.appendChild(el);

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

    expect(getStub()?.getAttribute('data-main-category-id')).toBe('3');
  });

  it('category-id が指す input の acms.category.change で mainCategoryId が更新される', async () => {
    document.body.innerHTML = `
      <input name="category_id" value="7" />
      <input id="hidden-sub-category" value="" />
    `;
    const categoryInput = document.querySelector<HTMLInputElement>('[name=category_id]')!;
    const el = createElement({ target: '#hidden-sub-category' });
    document.body.appendChild(el);

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

    categoryInput.value = '42';

    // Why not: 発火を waitFor の外で一度だけにすると CI で flaky になる。購読は
    // SubCategorySelectField の useEffect（passive effect）で登録されるため、stub が DOM に
    // 現れた時点ではまだ登録されておらず、登録前に流れた一発きりのイベントは失われて二度と
    // 反映されない。waitFor の中で発火し直せば購読が有効になるまでリトライできる（同じ値の
    // 再発火は冪等で、購読されなければタイムアウトして失敗するため検知力は落ちない）。
    await waitFor(() => {
      fireEvent(categoryInput, new CustomEvent('acms.category.change', { bubbles: true }));
      expect(getStub()?.getAttribute('data-main-category-id')).toBe('42');
    });
  });

  it('el.value = ... で target 先の input と表示の両方が更新される', async () => {
    document.body.innerHTML = `<input id="hidden-sub-category" value="" />`;
    const el = createElement({ target: '#hidden-sub-category' });
    document.body.appendChild(el);

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

    el.value = '9,10';

    expect(el.value).toBe('9,10');
    expect((document.getElementById('hidden-sub-category') as HTMLInputElement).value).toBe('9,10');
  });

  it('内部の選択変更で el.value が更新され、change イベントが発火する', async () => {
    document.body.innerHTML = `<input id="hidden-sub-category" value="" />`;
    const el = createElement({ target: '#hidden-sub-category' });
    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).toBe('new');
    expect(handleChange).toHaveBeenCalledTimes(1);
  });

  it('disabled 属性を切り替えても、既に選択済みの値は巻き戻らない', async () => {
    document.body.innerHTML = `<input id="hidden-sub-category" value="" />`;
    const el = createElement({ target: '#hidden-sub-category' });
    document.body.appendChild(el);

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

    fireEvent.click(document.querySelector('button')!);
    expect(el.value).toBe('new');

    el.disabled = true;

    await waitFor(() => {
      expect(getStub()?.getAttribute('data-disabled')).toBe('true');
    });
    expect(el.value).toBe('new');
  });

  it('切断すると React がアンマウントされ、再接続で再マウントされる', async () => {
    document.body.innerHTML = `<input id="hidden-sub-category" value="" />`;
    const el = createElement({ target: '#hidden-sub-category' });
    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 () => {
    document.body.innerHTML = `<input id="hidden-sub-category" value="" />`;
    const el = createElement({ target: '#hidden-sub-category' });
    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="sub-category-select-stub"]').length).toBe(1);
  });
});
