import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { SlotFillProvider } from '@ablogcms/components/slot-fill';
import UnitEditor from '@ablogcms/unit-editor/components/unit-editor';
import type { UnitTree, UnitTreeNode } from '@ablogcms/unit-editor/core/types/unit';
import { defaultEditorSettings } from '@ablogcms/unit-editor/config';
import type { UnitEditorSettings } from '@ablogcms/unit-editor/types';
import dispatchMediaField from '../../../../dispatch/media/dispatch-media-field';
import Edit from './edit';
import { renderFixture } from './__fixtures__/render-fixture-pure';
import { dragBySelector } from '../../../../../../../vitest.browser.drag-helpers';
// Browser Mode（実ブラウザ実行）では render-fixture.ts の node:fs 経由の読み込みが使えないため、
// Vite の `?raw` インポート（vitest.browser.d.ts で型定義）でフィクスチャ本体を文字列として取得し、
// fs 不要な純粋関数 renderFixture() に渡す。
import mediaFixtureRaw from './__fixtures__/media.html?raw';
import fieldGroupFixtureRaw from './__fixtures__/field-group.html?raw';

vi.mock('../../../../dispatch/dispatch-a-table-field', () => ({ default: vi.fn() }));
vi.mock('../../../../dispatch/dispatch-resize-image-cf', () => ({ default: vi.fn() }));
vi.mock('../../../../dispatch/dispatch-flatpicker', () => ({ default: vi.fn() }));
vi.mock('../../../../dispatch/dispatch-lite-editor-field', () => ({ default: vi.fn() }));
vi.mock('../../../../elements/upgrade-legacy-marks', () => ({ upgradeLegacyBlockEditors: vi.fn() }));
vi.mock('../../../../dispatch/dispatch-select2', () => ({ default: vi.fn() }));
vi.mock('../../../../dispatch/media/dispatch-media-field', () => ({ default: vi.fn() }));
vi.mock('../../../../dispatch/dispatch-rich-editor', () => ({ default: vi.fn() }));

// Why not: use-buildin-js.test.ts / edit.test.tsx（jsdom版）は「apply() が呼ばれた」ことをモック・
// スパイで検証するに留まる（jsdom では dnd-kit の実ドラッグを再現できないため）。本ファイルは
// カスタムユニット（フィクスチャHTML + 組み込みJS）を実 Chromium 上で実際にドラッグ移動し、
// 移動後も組み込みJSの適用（dispatch呼び出し・対象マークの再存在）が生き残る／再適用されることを
// 検証する。real jQuery（vitest.browser.setup.ts でグローバルに読み込み済み）が動くため、
// fieldgroupSortable の検出用に jsdom 版で使っていた `$` の差し替えスタブは不要。

let unitIdSeq = 0;
function createUnitId(name: string): string {
  unitIdSeq += 1;
  return `custom-${name}-${unitIdSeq}`;
}

function createCustomUnit(id: string, name: string): UnitTreeNode {
  return {
    id,
    type: 'custom',
    name,
    status: 'open',
    collapsed: false,
    children: [],
    attributes: {},
  };
}

/**
 * 本体の `group` ユニット（coreUnitDefs に組み込み済み。UnitEditor に渡す unitDefs で上書きしない限り
 * 自動的にマージされるため、テスト側で Edit を自作する必要はない）を使ってグループユニットを作る。
 * `attributes` は core/unit-defs/group/types.ts の GroupAttributes（class/tag）が必須。
 */
function createGroupUnit(id: string, children: UnitTree = []): UnitTreeNode {
  return {
    id,
    type: 'group',
    name: 'グループ',
    status: 'open',
    collapsed: false,
    children,
    attributes: { class: '', tag: 'div' },
  };
}

/**
 * 本体の group ユニット定義（core/unit-defs/group/edit.tsx）は `settings.groupUnit.tagOptions[0].value`
 * を無条件参照するため、既定の defaultEditorSettings（tagOptions: []）のままだと描画時に例外になる。
 * 最小限のタグ選択肢を補った settings を作る。
 */
function createSettingsWithGroupUnit(): UnitEditorSettings {
  return {
    ...defaultEditorSettings,
    groupUnit: {
      classOptions: [],
      tagOptions: [{ value: 'div', label: 'div' }],
    },
  };
}

/**
 * 本体の group ユニット（core/unit-defs/group/edit.tsx）は開始/終了バーの高さやマージンを acms.css
 * （ビルド済みスタイル）に依存しており、このテストは CSS ビルドを経由しないため、そのままでは
 * グループの外枠矩形と内部の子ユニット矩形がほぼ重なってしまう。dnd-kit の closestCorners は
 * 移動中アイテムの矩形とドロップ候補矩形の頂点間距離で判定するため、矩形がほぼ重なると隣接階層との
 * 境界を越えても `over` が変化しない（move-unit-drag.browser.test.tsx で実機確認済みの現象）。
 * テスト実行中だけ `.acms-admin-group-unit-content` に十分な内側パディングを与えて、子ユニットの矩形を
 * グループ自身の外枠矩形から明確に離す。
 */
function injectGroupUnitSpacingStyle(): void {
  if (document.getElementById('test-group-unit-spacing-style')) {
    return;
  }
  const style = document.createElement('style');
  style.id = 'test-group-unit-spacing-style';
  style.textContent = '.acms-admin-group-unit-content { padding: 60px; display: block; }';
  document.head.appendChild(style);
}

function createFetchMockByFixture(fixtures: Record<string, string>) {
  return vi.fn(async (url: string) => {
    const parsed = new URL(url, 'https://example.test/');
    const id = parsed.searchParams.get('id') ?? '';
    const fixtureRaw = fixtures[id];
    if (!fixtureRaw) {
      return { ok: false, text: async () => '' };
    }
    return { ok: true, text: async () => renderFixture(fixtureRaw, { id }) };
  });
}

beforeEach(() => {
  window.ACMS.Config = {
    ...window.ACMS.Config,
    root: '/',
    bid: '1',
    editorSetId: '1',
    suid: '1',
    aTableFieldMark: '.js-editable-table-field',
    LiteEditorMark: '.js-lite-editor-field',
    SmartBlockMark: '.js-smartblock,.js-paper-editor',
    blockEditorMark: '.js-block-editor',
    mediaFieldMark: '.js-media-field',
    select2Mark: '.js-select2',
    select2Threshold: 10,
    flatDatePicker: '.js-datepicker2',
    flatTimePicker: '.js-timepicker',
    resizeImageTargetMarkCF: '.js-img_resize_cf',
    fieldgroupSortableMark: '.js-fieldgroup-sortable',
  };
  window.ACMS.Library = {
    ...window.ACMS.Library,
    acmsLink: ((params: { searchParams?: Record<string, unknown> }) =>
      `/ajax/unit/render.html?${new URLSearchParams(params.searchParams as Record<string, string>).toString()}`) as unknown as typeof window.ACMS.Library.acmsLink,
  };
  window.csrfToken = 'test-csrf-token';
  // UnitEditor（本体トップレベルコンポーネント）は onCreate / onServerSideUnitRender の中で
  // レガシー互換の ACMS.dispatchEvent を呼ぶ（v3.2 未満との互換のため）。テスト用の最小 ACMS
  // モック（vitest.browser.setup.ts）には無いため、ここでスパイを補う。
  (window.ACMS as unknown as { dispatchEvent: unknown }).dispatchEvent = vi.fn();
  (window.ACMS as unknown as { Dispatch: Record<string, unknown> }).Dispatch = {
    fieldgroupSortable: vi.fn(),
    wysiwyg: { dispatch: vi.fn() },
  };
  // CommonUnitToolbar は useBreakpointValue（@ablogcms/react-hooks）経由で
  // --acms-admin-breakpoint-{xs,sm,md,lg,xl} を読む。本番はビルド済み acms.css
  // （ablogcms/themes/system/src/scss/global/_variables.scss の $grid-breakpoints）が供給するが、
  // このテストは CSS ビルドを経由しないため、実ブラウザの documentElement に直接同じ値を設定する。
  document.documentElement.style.setProperty('--acms-admin-breakpoint-xs', '0');
  document.documentElement.style.setProperty('--acms-admin-breakpoint-sm', '480px');
  document.documentElement.style.setProperty('--acms-admin-breakpoint-md', '768px');
  document.documentElement.style.setProperty('--acms-admin-breakpoint-lg', '1024px');
  document.documentElement.style.setProperty('--acms-admin-breakpoint-xl', '1440px');
});

afterEach(() => {
  vi.unstubAllGlobals();
  vi.clearAllMocks();
});

describe('カスタムユニットの実ドラッグ移動（Browser Mode / 実 Chromium）', () => {
  it('media.html / field-group.html の組み込みJSが初期適用され、実ドラッグで移動後も再適用される', async () => {
    const idMediaA = createUnitId('media-a');
    const idFieldGroup = createUnitId('field-group');
    // Why not: 隣接2件だけの直接スワップだと、React の reconciliation がキー（unit.id）で
    // 既存インスタンスをそのまま位置だけ入れ替えて再利用し、ServerSideRenderer が unmount/remount
    // されない（＝組み込みJSの再適用が起きない）ことを実機で確認した。実際のユーザー操作でも
    // 「先頭のユニットを2つ以上先まで」ドラッグする場合は途中のユニットを跨ぐため、EditorContent の
    // DragOver が複数回発火し、跨がれたユニット側で実際に unmount/remount が起きる。この経路を
    // 確実に踏むため、バッファとなる3件目（末尾）を用意し、先頭ユニットを末尾まで2ホップ移動させる。
    const idMediaBuffer = createUnitId('media-buffer');
    const fetchMock = createFetchMockByFixture({
      [idMediaA]: mediaFixtureRaw,
      [idFieldGroup]: fieldGroupFixtureRaw,
      [idMediaBuffer]: mediaFixtureRaw,
    });
    vi.stubGlobal('fetch', fetchMock);

    const defaultValue: UnitTree = [
      createCustomUnit(idMediaA, 'カスタム（メディア）'),
      createCustomUnit(idFieldGroup, 'カスタム（フィールドグループ）'),
      createCustomUnit(idMediaBuffer, 'カスタム（メディア・末尾）'),
    ];

    const { container } = render(
      <SlotFillProvider>
        <UnitEditor
          defaultValue={defaultValue}
          unitDefs={{ custom: { type: 'custom', name: 'カスタム', edit: Edit } }}
        />
      </SlotFillProvider>
    );

    // 初期適用: media-field / fieldgroupSortable がそれぞれの対象要素を検出する
    await waitFor(() => {
      expect(dispatchMediaField).toHaveBeenCalled();
      expect(window.ACMS.Dispatch.fieldgroupSortable).toHaveBeenCalled();
    });
    expect(container.querySelector(`[data-unit-id="${idMediaA}"] .js-media-field`)).not.toBeNull();
    expect(container.querySelector(`[data-unit-id="${idFieldGroup}"] .js-fieldgroup-sortable`)).not.toBeNull();
    expect(container.querySelector(`[data-unit-id="${idMediaBuffer}"] .js-media-field`)).not.toBeNull();

    // 先頭のメディアユニットを、フィールドグループを跨いで末尾のメディアユニットの位置まで実ドラッグする。
    // Why not: ドロップ先は対象カードの「ドラッグハンドル」ではなく「カード全体」を選ぶ。
    // dnd-kit の closestCorners は移動中アイテムの矩形とドロップ候補矩形の頂点間距離で判定するため、
    // 隣接カードの境界線付近（ハンドルはカード先頭にある）を狙うと移動元カードとの距離差が
    // ほとんどなくなり、境界をまたいでも over が移動元のまま変化しないことを実機で確認した
    // （vitest.browser.commands.ts の dragPointer コメント参照）。カード中心を狙えばこの曖昧さを避けられる。
    await dragBySelector(`[data-unit-id="${idMediaA}"] .acms-admin-btn-draggable`, `[data-unit-id="${idMediaBuffer}"]`);

    await waitFor(() => {
      const order = Array.from(container.querySelectorAll('[data-unit-id]')).map((el) =>
        el.getAttribute('data-unit-id')
      );
      expect(order.indexOf(idMediaA)).toBeGreaterThan(order.indexOf(idFieldGroup));
    });

    // Why not: 当初は「移動後に dispatch が再度呼ばれる（再適用される）」ことを assert する想定だった。
    // しかし実機で検証したところ、隣接する複数ユニットを跨ぐだけの直列な並べ替え（階層構造は不変）
    // では、React が unit.id をキーに既存の ServerSideRenderer インスタンスをそのまま位置だけ
    // 入れ替えて再利用し、unmount/remount も組み込みJSの再初期化も発生しない（＝一度適用された状態が
    // そのまま「生き残る」）ことを確認した（dispatchMediaField 等をクリアして 1.5 秒待っても再呼び出し
    // は無い）。これは効率的な挙動でありバグではないため、このテストでは "再適用" ではなく "生き残る"
    // （対象マークが移動後も消えない）ことを assert する。
    // 一方、React ツリー上の親が変わる階層移動（グループ内 ⇔ ルート）では実際に unmount/remount が
    // 起き、dispatch が移動前より多く呼ばれる（＝再適用される）ことを、下の2テストで確認している。
    expect(container.querySelector(`[data-unit-id="${idMediaA}"] .js-media-field`)).not.toBeNull();
    expect(container.querySelector(`[data-unit-id="${idFieldGroup}"] .js-fieldgroup-sortable`)).not.toBeNull();
    expect(container.querySelector(`[data-unit-id="${idMediaBuffer}"] .js-media-field`)).not.toBeNull();

    // 初回適用そのもの（移動前）は最低1回ずつ確実に呼ばれている（クリアしていないため、移動後も
    // 呼び出し履歴は保持されたまま）。
    expect(dispatchMediaField).toHaveBeenCalled();
    expect(window.ACMS.Dispatch.fieldgroupSortable).toHaveBeenCalled();
  });

  // Why not: 上のテストは「隣接ユニットを跨ぐ直列な並べ替え」であり、React が unit.id キーで
  // 既存インスタンスを再利用するため unmount/remount が起きない（＝「生き残る」ことしか検証できない）。
  // 元の「真っ白バグ」（移動元/移動先で React ツリー上の親が変わる階層移動）の経路を実ドラッグで
  // 通すには、実際に unmount/remount → SSR 再フェッチ → useBuildinJs.apply() の再実行が発生する
  // 「グループ内 ⇔ ルート」の階層移動が必要。ここでは dispatch の呼び出し回数が移動前より
  // 増えていることまで確認し、「再適用が実際に起きた」ことを検証する。
  it('グループ内のカスタムユニットを実ドラッグでルートへ移動すると、組み込みJSが再適用される（unmount/remount経路）', async () => {
    const idMediaInGroup = createUnitId('media-in-group');
    const idGroup = createUnitId('group');
    const idMediaRoot = createUnitId('media-root');
    const fetchMock = createFetchMockByFixture({
      [idMediaInGroup]: mediaFixtureRaw,
      [idMediaRoot]: mediaFixtureRaw,
    });
    vi.stubGlobal('fetch', fetchMock);
    injectGroupUnitSpacingStyle();

    const defaultValue: UnitTree = [
      createGroupUnit(idGroup, [createCustomUnit(idMediaInGroup, 'カスタム（グループ内メディア）')]),
      createCustomUnit(idMediaRoot, 'カスタム（ルートメディア）'),
    ];

    const { container } = render(
      <SlotFillProvider>
        <UnitEditor
          defaultValue={defaultValue}
          settings={createSettingsWithGroupUnit()}
          unitDefs={{ custom: { type: 'custom', name: 'カスタム', edit: Edit } }}
        />
      </SlotFillProvider>
    );

    // 初期適用: グループ内・ルートの両方の media-field が検出される
    await waitFor(() => {
      expect(dispatchMediaField).toHaveBeenCalled();
    });
    await waitFor(() => {
      expect(
        container.querySelector(`[data-unit-id="${idGroup}"] [data-unit-id="${idMediaInGroup}"] .js-media-field`)
      ).not.toBeNull();
      expect(container.querySelector(`[data-unit-id="${idMediaRoot}"] .js-media-field`)).not.toBeNull();
    });

    const callsBeforeDrag = vi.mocked(dispatchMediaField).mock.calls.length;

    // グループ内のメディアユニットを、ルートのメディアユニットのカード全体へ実ドラッグして外に出す
    await dragBySelector(
      `[data-unit-id="${idMediaInGroup}"] .acms-admin-btn-draggable`,
      `[data-unit-id="${idMediaRoot}"]`
    );

    // 移動先（ルート階層）で SSR コンテナが非空になっている（= 真っ白バグの regression）
    await waitFor(() => {
      const rootLevelInGroupUnit = Array.from(container.querySelectorAll(`[data-unit-id="${idMediaInGroup}"]`)).find(
        (el) => el.parentElement?.closest('[data-unit-id]') === null
      );
      expect(rootLevelInGroupUnit?.querySelector('.js-media-field')).not.toBeNull();
    });

    // グループの DOM 配下には残っていないこと
    expect(container.querySelector(`[data-unit-id="${idGroup}"] [data-unit-id="${idMediaInGroup}"]`)).toBeNull();

    // 移動先のユニット（ルートに残っている元々のメディアユニット）も消えていないこと
    expect(container.querySelector(`[data-unit-id="${idMediaRoot}"] .js-media-field`)).not.toBeNull();

    // 本題: unmount/remount によって useBuildinJs.apply() が再実行され、dispatch が移動前より
    // 多く呼ばれている（＝「生き残る」ではなく「再適用された」ことの確認）
    await waitFor(() => {
      expect(vi.mocked(dispatchMediaField).mock.calls.length).toBeGreaterThan(callsBeforeDrag);
    });
  });

  // 余力: 逆方向（ルート → グループ内）でも同様に unmount/remount 経路を通ることを確認する
  it('ルートのカスタムユニットを実ドラッグでグループ内へ移動すると、組み込みJSが再適用される（unmount/remount経路）', async () => {
    const idMediaExisting = createUnitId('media-existing');
    const idGroup = createUnitId('group');
    const idMediaRoot = createUnitId('media-root');
    const fetchMock = createFetchMockByFixture({
      [idMediaExisting]: mediaFixtureRaw,
      [idMediaRoot]: mediaFixtureRaw,
    });
    vi.stubGlobal('fetch', fetchMock);
    injectGroupUnitSpacingStyle();

    const defaultValue: UnitTree = [
      createGroupUnit(idGroup, [createCustomUnit(idMediaExisting, 'カスタム（グループ内メディア）')]),
      createCustomUnit(idMediaRoot, 'カスタム（ルートメディア）'),
    ];

    const { container } = render(
      <SlotFillProvider>
        <UnitEditor
          defaultValue={defaultValue}
          settings={createSettingsWithGroupUnit()}
          unitDefs={{ custom: { type: 'custom', name: 'カスタム', edit: Edit } }}
        />
      </SlotFillProvider>
    );

    await waitFor(() => {
      expect(
        container.querySelector(`[data-unit-id="${idGroup}"] [data-unit-id="${idMediaExisting}"] .js-media-field`)
      ).not.toBeNull();
      expect(container.querySelector(`[data-unit-id="${idMediaRoot}"] .js-media-field`)).not.toBeNull();
    });

    const callsBeforeDrag = vi.mocked(dispatchMediaField).mock.calls.length;

    // ルートのメディアユニットを、グループ内の既存メディアユニットのカード全体へ実ドラッグして
    // グループの子として移動させる
    await dragBySelector(
      `[data-unit-id="${idMediaRoot}"] .acms-admin-btn-draggable`,
      `[data-unit-id="${idMediaExisting}"]`
    );

    await waitFor(() => {
      expect(
        container.querySelector(`[data-unit-id="${idGroup}"] [data-unit-id="${idMediaRoot}"] .js-media-field`)
      ).not.toBeNull();
    });

    await waitFor(() => {
      expect(vi.mocked(dispatchMediaField).mock.calls.length).toBeGreaterThan(callsBeforeDrag);
    });
  });
});
