import { describe, it, expect, vi, afterEach } from 'vitest';
import { waitFor, fireEvent } from '@testing-library/react';
import { createTagDefaultValue, mountTagSelectFromAttributes } from './mount-tag-select';
import type { TagOption } from './types';

vi.mock('./components/tag-select/tag-select', () => ({
  default: ({
    defaultValue,
    onChange,
    id,
    inputId,
    isDisabled,
    form,
    name,
  }: {
    defaultValue: TagOption[];
    onChange: (value: TagOption[]) => void;
    id?: string;
    inputId?: string;
    isDisabled?: boolean;
    form?: string;
    name?: string;
  }) => (
    <div
      data-testid="tag-select-stub"
      data-id={id}
      data-input-id={inputId}
      data-disabled={String(!!isDisabled)}
      data-form={form}
      data-name={name}
    >
      <span data-testid="default-value">{JSON.stringify(defaultValue)}</span>
      <button type="button" onClick={() => onChange([{ label: 'new', value: 'new' }])}>
        change
      </button>
    </div>
  ),
}));

describe('createTagDefaultValue', () => {
  it('カンマ区切りの文字列を TagOption[] に変換する', () => {
    expect(createTagDefaultValue('a, b')).toEqual([
      { label: 'a', value: 'a' },
      { label: 'b', value: 'b' },
    ]);
  });

  it('空文字は空配列になる', () => {
    expect(createTagDefaultValue('')).toEqual([]);
  });
});

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

  it('target も host 内の hidden input も無い場合はエラーを投げる', () => {
    const host = document.createElement('div');
    expect(() => mountTagSelectFromAttributes(host, {})).toThrow('Not found target attribute or hidden input element!');
  });

  it('target 属性の参照先の input が見つからない場合はエラーを投げる', () => {
    const host = document.createElement('div');
    expect(() => mountTagSelectFromAttributes(host, { target: '#missing' })).toThrow('Not found input element!');
  });

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

    const result = mountTagSelectFromAttributes(host, { defaultValue: 'a, b' });

    expect(result.input).toBe(input);

    await waitFor(() => {
      expect(document.querySelector('[data-testid="tag-select-stub"]')).not.toBeNull();
    });
    expect(JSON.parse(document.querySelector('[data-testid="default-value"]')?.textContent || '[]')).toEqual([
      { label: 'a', value: 'a' },
      { label: 'b', value: 'b' },
    ]);

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

  // Why not: host 自身へ直接 React をレンダリングすると、host の子孫に置いた hidden input が
  // React のレンダリング結果で上書きされて消えてしまう（block-editor の mountBlockEditorField と
  // 同じ理由）。専用のマウントコンテナへレンダリングすることで、host の他の子孫（hidden input）を
  // 壊さないことを特徴づける。
  it('host 内の hidden input を使うモードでは、host 自身ではなく別のマウントコンテナへレンダリングし、input を壊さない', async () => {
    const host = document.createElement('div');
    host.innerHTML = `<input type="hidden" name="tag" value="" />`;
    document.body.appendChild(host);
    const input = host.querySelector('input') as HTMLInputElement;

    mountTagSelectFromAttributes(host, {});

    await waitFor(() => {
      expect(document.querySelector('[data-testid="tag-select-stub"]')).not.toBeNull();
    });

    // hidden input は host の子孫としてそのまま残っている（React に上書きされていない）。
    expect(input.isConnected).toBe(true);
    expect(host.contains(input)).toBe(true);
  });

  // target 属性がある場合は、host 内に hidden input があっても target を優先する
  // （既存動作の後方互換性を優先する）。
  it('target 属性がある場合は host 内の hidden input より優先される', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="" />`;
    const host = document.createElement('div');
    host.innerHTML = `<input type="hidden" name="tag" value="" />`;
    document.body.appendChild(host);
    const externalInput = document.getElementById('hidden-tag') as HTMLInputElement;

    const result = mountTagSelectFromAttributes(host, { target: '#hidden-tag' });

    expect(result.input).toBe(externalInput);
  });

  it('data-default-value を defaultValue として使用し、hidden input の現在値には依存しない', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="should-not-be-used" />`;
    const host = document.createElement('div');
    document.body.appendChild(host);

    mountTagSelectFromAttributes(host, {
      target: '#hidden-tag',
      defaultValue: 'x, y',
      id: 'tag-id',
      inputId: 'tag-input-id',
      isDisabled: 'true',
      form: 'entry-form',
      name: 'tag-name',
    });

    await waitFor(() => {
      expect(document.querySelector('[data-testid="tag-select-stub"]')).not.toBeNull();
    });

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

    const stub = document.querySelector('[data-testid="tag-select-stub"]')!;
    expect(stub.getAttribute('data-id')).toBe('tag-id');
    expect(stub.getAttribute('data-input-id')).toBe('tag-input-id');
    expect(stub.getAttribute('data-disabled')).toBe('true');
    expect(stub.getAttribute('data-form')).toBe('entry-form');
    expect(stub.getAttribute('data-name')).toBe('tag-name');
  });

  it('選択変更で参照先の hidden input が更新され、acms.tag.change が発火する', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="" />`;
    const host = document.createElement('div');
    document.body.appendChild(host);
    const input = document.getElementById('hidden-tag') as HTMLInputElement;
    const handleChange = vi.fn();
    input.addEventListener('acms.tag.change', handleChange);

    const { input: resolvedInput } = mountTagSelectFromAttributes(host, { target: '#hidden-tag' });
    expect(resolvedInput).toBe(input);

    await waitFor(() => {
      expect(document.querySelector('[data-testid="tag-select-stub"]')).not.toBeNull();
    });

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

    expect(input.value).toBe('new');
    expect(handleChange).toHaveBeenCalledTimes(1);
  });

  it('update() は defaultValue を渡さないと空値扱いになる（呼び出し側が都度最新値を渡す契約）', async () => {
    document.body.innerHTML = `<input id="hidden-tag" value="" />`;
    const host = document.createElement('div');
    document.body.appendChild(host);

    const { controller, input } = mountTagSelectFromAttributes(host, {
      target: '#hidden-tag',
      defaultValue: 'a',
    });

    await waitFor(() => {
      expect(document.querySelector('[data-testid="default-value"]')?.textContent).toBe(
        JSON.stringify([{ label: 'a', value: 'a' }])
      );
    });

    input.value = 'b';
    controller.update({ defaultValue: input.value, isDisabled: true });

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

    controller.unmount();
    expect(document.querySelector('[data-testid="tag-select-stub"]')).toBeNull();
  });
});
