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

vi.mock('@features/tag/components/tag-select/tag-select', () => ({
  default: ({
    defaultValue,
    onChange,
    isDisabled,
    className,
  }: {
    defaultValue: { label: string; value: string }[];
    onChange: (value: { label: string; value: string }[]) => void;
    isDisabled?: boolean;
    className?: string;
  }) => (
    <div data-testid="tag-select-stub" 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="tag-select-stub"]');

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

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

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

  it('target 先の input へ TagSelect をマウントし、自身には hidden input を持たない', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="a,b" />`;
    const el = createElement({ target: '#hidden-tag', 'default-value': 'a,b' });
    document.body.appendChild(el);

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

    expect(el.querySelector('input')).toBeNull();
    expect(JSON.parse(document.querySelector('[data-testid="default-value"]')?.textContent || '[]')).toEqual([
      { label: 'a', value: 'a' },
      { label: 'b', value: 'b' },
    ]);
  });

  it('target 属性も host 内の hidden input も無い場合は 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();
  });

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

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

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

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

    el.value = 'x,y';

    expect(el.value).toBe('x,y');
    expect((document.getElementById('hidden-tag') as HTMLInputElement).value).toBe('x,y');

    await waitFor(() => {
      expect(JSON.parse(document.querySelector('[data-testid="default-value"]')?.textContent || '[]')).toEqual([
        { label: 'x', value: 'x' },
        { label: 'y', value: 'y' },
      ]);
    });
  });

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

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

    // ユーザーが選択を変更した後で disabled を切り替えても、その値が保たれることを確認する
    fireEvent.click(document.querySelector('button')!);
    expect(el.value).toBe('new');

    el.disabled = true;

    await waitFor(() => {
      expect(getStub()?.getAttribute('data-disabled')).toBe('true');
    });
    expect(el.hasAttribute('disabled')).toBe(true);
    expect(JSON.parse(document.querySelector('[data-testid="default-value"]')?.textContent || '[]')).toEqual([
      { label: 'new', value: 'new' },
    ]);

    el.disabled = false;

    await waitFor(() => {
      expect(getStub()?.getAttribute('data-disabled')).toBe('false');
    });
  });

  it('is-disabled="true" を初期状態として disabled 属性へ正規化する', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="" />`;
    const el = createElement({ target: '#hidden-tag', 'is-disabled': 'true' });
    document.body.appendChild(el);

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

  it('切断すると React がアンマウントされる', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="" />`;
    const el = createElement({ target: '#hidden-tag' });
    document.body.appendChild(el);

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

    el.remove();

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

  it('host の root-class 属性が実際にレンダリングされる TagSelect のルート要素へ転送される', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="" />`;
    const el = createElement({ target: '#hidden-tag' });
    el.setAttribute('root-class', 'my-custom-class');
    document.body.appendChild(el);

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

    // host（<acms-tag-select>）は display: contents で自身のボックスを持たないため、
    // root-class は実際にレンダリングされる要素側で反映される必要がある。
    expect(getStub()?.className).toBe('my-custom-class');
    // root-class は class と違い host 自身の見た目に意味を持つ標準属性ではないため取り除かない。
    expect(el.getAttribute('root-class')).toBe('my-custom-class');
  });

  it('接続 → 切断 → 再接続でも例外にならず、再度マウントされる', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="" />`;
    const el = createElement({ target: '#hidden-tag' });
    document.body.appendChild(el);

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

    el.remove();
    expect(() => document.body.appendChild(el)).not.toThrow();

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

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