/**
 * patches/frontend/trumbowyg+2.27.3.patch で当てている Trumbowyg 本体パッチの動作検証。
 *
 * このパッチは、WYSIWYG エディターの日本語入力に関する一連のサポート報告を
 * きっかけに当てているもので、それぞれ次の不具合を防いでいる。
 *
 * - 文字色やサイズを装飾した日本語を入力し、Enter で確定すると装飾が消える
 * - 装飾したテキストの途中で改行し、その改行を削除すると装飾が外れる
 * - 日本語入力の確定直後に保存すると、入力した内容が一部保存されない
 * - リンク作成モーダルの入力欄に入力できず、背面のエディター本文が編集されてしまう
 *
 * lib/wysiwyg.js は Trumbowyg をそのまま起動する薄いラッパーのため、
 * ここではパッチ対象である Trumbowyg 本体の振る舞いを検証する。
 */
import $ from 'jquery';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';

interface TrumbowygInstance {
  $ed: JQuery;
  restoreRange: () => void;
  syncTextarea: () => void;
  semanticCode: (force?: boolean, full?: boolean, keepRange?: boolean) => void;
  toggleSpan: (addFlag?: boolean) => void;
}

const createEditor = (): { textarea: HTMLTextAreaElement; instance: TrumbowygInstance } => {
  const textarea = document.createElement('textarea');
  document.body.appendChild(textarea);
  const $textarea = $(textarea);
  // 本番設定（ACMS.Config.wysiwygConfig）と同じ semantic オプションで初期化する
  $textarea.trumbowyg({ semantic: { div: 'div' } });
  const instance = $textarea.data('trumbowyg') as TrumbowygInstance;
  return { textarea, instance };
};

const selectEditorText = (instance: TrumbowygInstance) => {
  const textNode = instance.$ed.find('p').contents().get(0);
  if (!textNode) {
    throw new Error('エディター内のテキストノードが取得できませんでした');
  }
  const range = document.createRange();
  range.selectNodeContents(textNode);
  const selection = document.getSelection();
  if (!selection) {
    throw new Error('Selection が取得できませんでした');
  }
  selection.removeAllRanges();
  selection.addRange(range);
};

beforeAll(async () => {
  // trumbowyg はグローバルの jQuery を前提とするため、import 前にグローバルへ公開する
  Object.assign(globalThis, { jQuery: $, $ });
  // jsdom はレイアウトを持たず :visible が常に false になるため、
  // 実ブラウザと同じ「エディター表示中」の分岐（エディター → textarea への同期）を通す
  $.expr.pseudos.visible = () => true;
  // jsdom は document.execCommand を実装していないため、
  // 空エディターでの keyup 時（formatBlock 実行）に TypeError にならないようスタブする
  document.execCommand = vi.fn(() => true);
  await import('trumbowyg');
  // アイコン未設定の警告（You must define svgPath）を抑止する
  $.trumbowyg.svgPath = false;
});

afterEach(() => {
  document.getSelection()?.removeAllRanges();
  document.body.innerHTML = '';
});

describe('エディターからフォーカスが外れたとき', () => {
  it('リンク作成モーダルへフォーカスが移っても、エディターがフォーカスを奪い返さない', () => {
    // 不具合: モーダルの「URL」欄に入力しようとすると背面のエディター本文が
    // 編集されてしまい、入力した URL が本文にテキストとして挿入されてしまう

    // Arrange
    const { instance } = createEditor();
    instance.$ed.html('<p>リンクにするテキスト</p>');
    selectEditorText(instance);
    const restoreRangeSpy = vi.spyOn(instance, 'restoreRange');

    // Act: モーダルの入力欄へフォーカスが移った状態（エディターの blur）を再現
    instance.$ed.trigger('blur');

    // Assert: 選択範囲を復元（restoreRange）するとブラウザがフォーカスを
    // エディターへ戻してしまうため、呼ばれてはいけない
    expect(restoreRangeSpy).not.toHaveBeenCalled();
  });

  it('編集した内容が保存用の textarea に反映される', () => {
    // 不具合: 日本語入力の確定直後にすぐ保存ボタンを押すと、
    // 入力した内容が一部保存されないことがあった

    // Arrange
    const { textarea, instance } = createEditor();
    instance.$ed.html('<p>確定直後のテキスト</p>');

    // Act
    instance.$ed.trigger('blur');

    // Assert
    expect(textarea.value).toContain('確定直後のテキスト');
  });
});

describe('日本語入力（IME）の変換確定', () => {
  // 不具合: 文字色やサイズを装飾した日本語を入力し、Enter で変換を確定すると、
  // 確定の Enter が「改行」として二重処理され、装飾が消えてしまう

  it('変換中に Enter で確定しても、改行として二重処理しない', () => {
    // Arrange
    const { instance } = createEditor();
    instance.$ed.html('<p>へんかんちゅう</p>');
    const semanticCodeSpy = vi.spyOn(instance, 'semanticCode');

    // Act: 変換中（compositionstart 後）に Enter で候補を確定
    instance.$ed.trigger('compositionstart');
    instance.$ed.trigger($.Event('keyup', { which: 13, keyCode: 13 }));

    // Assert: 本文の再整形（semanticCode）が走ると装飾が消えるため、呼ばれてはいけない
    expect(semanticCodeSpy).not.toHaveBeenCalled();
  });

  it('IME が処理中であることを示すキー入力（keyCode 229）も、二重処理しない', () => {
    // Arrange
    const { instance } = createEditor();
    const semanticCodeSpy = vi.spyOn(instance, 'semanticCode');

    // Act
    instance.$ed.trigger($.Event('keyup', { which: 229, keyCode: 229 }));

    // Assert
    expect(semanticCodeSpy).not.toHaveBeenCalled();
  });

  it('変換終了の直後に確定の Enter が届いても、二重処理しない（Safari / Firefox）', () => {
    // Safari / Firefox では「変換終了（compositionend）→ 確定の Enter」の順で
    // イベントが届くため、変換終了後 100ms は変換中の扱いを続ける必要がある

    // Arrange
    const { instance } = createEditor();
    const semanticCodeSpy = vi.spyOn(instance, 'semanticCode');

    // Act
    instance.$ed.trigger('compositionstart');
    instance.$ed.trigger('compositionend');
    instance.$ed.trigger($.Event('keyup', { which: 13, keyCode: 13 }));

    // Assert
    expect(semanticCodeSpy).not.toHaveBeenCalled();
  });

  it('変換が終わって少し経ってから押した Enter は、通常の改行として処理する', async () => {
    // Arrange
    const { instance } = createEditor();
    const semanticCodeSpy = vi.spyOn(instance, 'semanticCode');

    // Act: 変換確定が完了した（100ms 以上経過した）あとの Enter 入力
    instance.$ed.trigger('compositionstart');
    instance.$ed.trigger('compositionend');
    await new Promise((resolve) => {
      setTimeout(resolve, 150);
    });
    instance.$ed.trigger($.Event('keyup', { which: 13, keyCode: 13 }));

    // Assert: 日本語入力対応のせいで通常の改行まで止めてはいけない
    expect(semanticCodeSpy).toHaveBeenCalled();
  });

  it('変換確定の直後に次の変換を始めても、変換中の扱いが解除されない', async () => {
    // 変換確定（compositionend）でセットされる「100ms 後に変換中の扱いを解除する」
    // タイマーは、次の変換開始時にクリアされる必要がある。クリアしないと、
    // 連続して日本語入力したときに 2 回目の変換中で二重処理が再発する

    // Arrange
    const { instance } = createEditor();
    const semanticCodeSpy = vi.spyOn(instance, 'semanticCode');

    // Act: 1 回目の変換を確定し、間を置かずに次の変換を開始する
    instance.$ed.trigger('compositionstart');
    instance.$ed.trigger('compositionend');
    instance.$ed.trigger('compositionstart');

    // 1 回目の確定から 100ms 以上経過したあと、2 回目の変換中に Enter で確定
    await new Promise((resolve) => {
      setTimeout(resolve, 150);
    });
    instance.$ed.trigger($.Event('keyup', { which: 13, keyCode: 13 }));

    // Assert: 2 回目の変換はまだ継続中なので、二重処理してはいけない
    expect(semanticCodeSpy).not.toHaveBeenCalled();
  });

  it('変換中の Enter では、装飾を壊す原因になる span の整理準備を行わない', () => {
    // Arrange
    const { instance } = createEditor();
    const toggleSpanSpy = vi.spyOn(instance, 'toggleSpan');

    // Act
    instance.$ed.trigger('compositionstart');
    instance.$ed.trigger($.Event('keydown', { which: 13, keyCode: 13 }));

    // Assert
    expect(toggleSpanSpy).not.toHaveBeenCalled();
  });

  it('日本語入力を使っていない通常の Enter は、これまでどおり処理する', () => {
    // Arrange
    const { instance } = createEditor();
    const semanticCodeSpy = vi.spyOn(instance, 'semanticCode');

    // Act
    instance.$ed.trigger($.Event('keyup', { which: 13, keyCode: 13 }));

    // Assert: 日本語入力対応のせいで英数字入力時の処理まで止めてはいけない
    expect(semanticCodeSpy).toHaveBeenCalled();
  });
});

describe('文字の装飾（色・サイズなど）の保持', () => {
  // 不具合: 装飾したテキストの途中で改行し、その改行を Backspace / Delete で
  // 削除すると、装飾が外れてしまう

  it('色やクラスを指定した装飾は、エディターの整理処理で消えない', () => {
    // Arrange
    const { instance } = createEditor();
    instance.$ed.html(
      '<p><span style="color: red;">スタイル付き</span><span class="marker">クラス付き</span><span>属性なし</span></p>'
    );

    // Act: Chrome が自動生成した余計な span を取り除く整理処理
    instance.toggleSpan();

    // Assert: ユーザーが付けた装飾（style / class 付きの span）は保持され、
    // 属性なし（Chrome 生成）の span だけが取り除かれる
    expect(instance.$ed.find('span[style]')).toHaveLength(1);
    expect(instance.$ed.find('span.marker')).toHaveLength(1);
    expect(instance.$ed.find('span')).toHaveLength(2);
    expect(instance.$ed.text()).toBe('スタイル付きクラス付き属性なし');
  });

  it('Backspace で改行を削除しても、文字の装飾が消えない', () => {
    // Arrange
    const { instance } = createEditor();
    instance.$ed.html('<p><span style="color: red;">装飾テキスト</span></p>');

    // Act: 装飾テキストの近くで Backspace（改行削除）を押す操作を再現
    instance.$ed.trigger($.Event('keydown', { which: 8, keyCode: 8 }));
    instance.$ed.trigger($.Event('keyup', { which: 8, keyCode: 8 }));

    // Assert: 装飾が外れず、整理処理の作業用フラグも残らないこと
    const span = instance.$ed.find('span');
    expect(span).toHaveLength(1);
    expect(span.attr('style')).toBe('color: red;');
    expect(span.attr('data-tbw-flag')).toBeUndefined();
  });
});
