import { vi, type Mock } from 'vitest';
import type { Editor } from '@tiptap/core';

/**
 * tiptap Editor の chain() / isActive() / can() / state を最小限再現するモック。
 *
 * chain() は fluent インタフェース。`.run()` で終端、それ以外のメソッドは this を返す。
 * 呼び出されたメソッド名と引数の列が `chainCalls` に蓄積される。
 *
 * isActive と can は `vi.fn` なので、各テストで戻り値を差し替えられる。
 *
 * `state.selection.$from` は depth / before / node を持つ最小モック。
 */
export interface MockEditor {
  editor: Editor;
  chainCalls: Array<{ method: string; args: unknown[] }>;
  isActive: Mock;
  can: Mock;
  setSelectionDepth: (depth: number) => void;
  setSelectionHead: (head: number) => void;
  setNodeAt: (depth: number, typeName: string) => void;
  /** 任意の chain メソッド (例: 'setParagraph') が「.run() 終了までの間に」呼ばれたか */
  expectChainCalled: (method: string) => boolean;
  /** 指定 chain メソッドの呼び出し時の args を取得 */
  getChainCallArgs: (method: string) => unknown[] | undefined;
}

export default function createMockEditor(): MockEditor {
  const chainCalls: Array<{ method: string; args: unknown[] }> = [];
  const nodeMap = new Map<number, { type: { name: string } }>();
  const state = {
    selection: {
      head: 0,
      $from: {
        depth: 1,
        before: () => 0,
        node: (depth: number) => nodeMap.get(depth) ?? { type: { name: 'doc' } },
      },
    },
  };

  const chainProxy: unknown = new Proxy(
    {},
    {
      get(_target, prop: string) {
        if (prop === 'run') {
          return () => undefined;
        }
        return (...args: unknown[]) => {
          chainCalls.push({ method: prop, args });
          return chainProxy;
        };
      },
    }
  );

  const isActive = vi.fn(() => false);
  const can = vi.fn(
    () =>
      new Proxy(
        {},
        {
          get() {
            return () => true;
          },
        }
      )
  );

  const editor = {
    chain: () => chainProxy,
    isActive,
    can,
    state,
  } as unknown as Editor;

  return {
    editor,
    chainCalls,
    isActive,
    can,
    setSelectionDepth: (depth) => {
      state.selection.$from.depth = depth;
    },
    setSelectionHead: (head) => {
      state.selection.head = head;
    },
    setNodeAt: (depth, typeName) => {
      nodeMap.set(depth, { type: { name: typeName } });
    },
    expectChainCalled: (method) => chainCalls.some((c) => c.method === method),
    getChainCallArgs: (method) => chainCalls.find((c) => c.method === method)?.args,
  };
}
