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

vi.mock('@features/category/components/category-select/category-select', () => ({
  default: ({
    defaultValue,
    onChange,
    isDisabled,
    narrowDown,
    noOption,
    isCreatable,
    className,
  }: {
    defaultValue?: string;
    onChange: (value: { label: string; value: string } | null) => void;
    isDisabled?: boolean;
    narrowDown?: boolean;
    noOption?: boolean;
    isCreatable?: boolean;
    className?: string;
  }) => (
    <div
      data-testid="category-select-stub"
      data-disabled={String(!!isDisabled)}
      data-narrow-down={String(!!narrowDown)}
      data-no-option={String(!!noOption)}
      data-is-creatable={String(!!isCreatable)}
      className={className}
    >
      <span data-testid="default-value">{defaultValue ?? ''}</span>
      <button type="button" onClick={() => onChange({ label: 'new', value: '99' })}>
        change
      </button>
      <button type="button" data-testid="clear" onClick={() => onChange(null)}>
        clear
      </button>
    </div>
  ),
}));

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

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

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

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

  it('target 先の input へ CategorySelect をマウントする', async () => {
    document.body.innerHTML = `<input id="hidden-category" value="5" />`;
    const el = createElement({ target: '#hidden-category', 'default-value': '5' });
    document.body.appendChild(el);

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

    expect(document.querySelector('[data-testid="default-value"]')?.textContent).toBe('5');
  });

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

    await waitFor(() => {
      expect(getStub()).not.toBeNull();
    });
    expect(document.querySelector('[data-testid="default-value"]')?.textContent).toBe('5');
    expect(input.isConnected).toBe(true);

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

  it('host の root-class 属性が実際にレンダリングされる CategorySelect のルート要素へ転送される', async () => {
    document.body.innerHTML = `<input id="hidden-category" value="" />`;
    const el = createElement({ target: '#hidden-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('narrow-down / no-option / creatable 系の属性が options に反映される', async () => {
    document.body.innerHTML = `<input id="hidden-category" value="" />`;
    const el = createElement({
      target: '#hidden-category',
      'narrow-down': 'true',
      'no-option': 'true',
      'is-creatable': 'true',
    });
    document.body.appendChild(el);

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

    const stub = getStub()!;
    expect(stub.getAttribute('data-narrow-down')).toBe('true');
    expect(stub.getAttribute('data-no-option')).toBe('true');
    expect(stub.getAttribute('data-is-creatable')).toBe('true');
  });

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

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

    el.value = '42';

    expect(el.value).toBe('42');
    expect((document.getElementById('hidden-category') as HTMLInputElement).value).toBe('42');

    await waitFor(() => {
      expect(document.querySelector('[data-testid="default-value"]')?.textContent).toBe('42');
    });
  });

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

  it('選択解除（null）で el.value が空文字になる', async () => {
    document.body.innerHTML = `<input id="hidden-category" value="5" />`;
    const el = createElement({ target: '#hidden-category' });
    document.body.appendChild(el);

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

    fireEvent.click(document.querySelector('[data-testid="clear"]')!);

    expect(el.value).toBe('');
  });

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

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

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

    el.disabled = true;

    await waitFor(() => {
      expect(getStub()?.getAttribute('data-disabled')).toBe('true');
    });
    expect(document.querySelector('[data-testid="default-value"]')?.textContent).toBe('99');
  });

  it('切断すると React がアンマウントされ、再接続で再マウントされる', async () => {
    document.body.innerHTML = `<input id="hidden-category" value="" />`;
    const el = createElement({ target: '#hidden-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 を壊さず・二重マウントもしないことを特徴づける
  // （詳細は elements/block-editor-element.ts の同名テスト参照）。
  it('マイクロタスクが実行される前に再接続された場合（物理的な DOM 移動）は unmount せず、二重マウントもしない', async () => {
    document.body.innerHTML = `<input id="hidden-category" value="" />`;
    const el = createElement({ target: '#hidden-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="category-select-stub"]').length).toBe(1);
  });
});
