import { afterEach, describe, expect, it } from "vite-plus/test";

import { BlockNoteEditor } from "./BlockNoteEditor.js";

/**
 * @vitest-environment jsdom
 */

// Track editors created in each test so we can unmount them in afterEach —
// otherwise prosemirror-view's DOMObserver leaves a setTimeout alive that
// fires after vitest tears down jsdom, throwing
// `ReferenceError: document is not defined` and failing the run.
const activeEditors: BlockNoteEditor<any, any, any>[] = [];

afterEach(() => {
  while (activeEditors.length) {
    activeEditors.pop()!.unmount();
  }
});

/**
 * Performance regression tests for issue #2595:
 * Typing/echo lag with many blocks (~50k chars total).
 *
 * The root cause was several plugins performing O(n) full-document traversals
 * on every ProseMirror transaction, where n = total number of blocks.
 * The fix limits traversals to only the changed ranges.
 *
 * These tests verify the optimization by:
 * 1. Checking plugin state reflects scoped (not full) traversal
 * 2. Measuring that transaction time scales sub-linearly with doc size
 */

function createEditorWithBlocks(
  blockCount: number,
  blockType: "heading" | "paragraph" | "numberedListItem" = "heading",
) {
  const editor = BlockNoteEditor.create();
  editor.mount(document.createElement("div"));
  activeEditors.push(editor);
  const blocks = [];
  for (let i = 0; i < blockCount; i++) {
    blocks.push({
      type: blockType as any,
      content: `Block number ${i} with some text to simulate a real document`,
      ...(blockType === "heading" ? { props: { level: 1 } } : {}),
    });
  }
  editor.replaceBlocks(editor.document, blocks as any);
  return editor;
}

describe("Performance: transaction processing scales sub-linearly (#2595)", () => {
  // Compare timing between a small and large document.
  // At 10k blocks the ratio is dominated by ProseMirror's DecorationSet.map()
  // which is inherently O(n). The thresholds verify BlockNote plugins don't
  // add significant overhead on top of that.
  const SMALL = 100;
  const LARGE = 5000;
  const ITERATIONS = 5;

  function measureAvgInsertTime(
    editor: BlockNoteEditor<any, any, any>,
    pos: number,
  ) {
    const view = editor._tiptapEditor.view;
    // Warm up
    view.dispatch(view.state.tr.insertText("w", pos));

    const start = performance.now();
    for (let i = 0; i < ITERATIONS; i++) {
      view.dispatch(view.state.tr.insertText("x", pos));
    }
    return (performance.now() - start) / ITERATIONS;
  }

  it("heading blocks: typing at end", { timeout: 30_000 }, () => {
    const smallEditor = createEditorWithBlocks(SMALL, "heading");
    const largeEditor = createEditorWithBlocks(LARGE, "heading");

    const smallAvg = measureAvgInsertTime(
      smallEditor,
      smallEditor._tiptapEditor.view.state.doc.content.size - 4,
    );
    const largeAvg = measureAvgInsertTime(
      largeEditor,
      largeEditor._tiptapEditor.view.state.doc.content.size - 4,
    );
    const ratio = largeAvg / smallAvg;

    // eslint-disable-next-line no-console
    console.log(
      `Heading blocks (end): ${SMALL}=${smallAvg.toFixed(3)}ms, ${LARGE}=${largeAvg.toFixed(3)}ms, ratio=${ratio.toFixed(2)}x`,
    );

    // Ratio scales roughly linearly with block count due to ProseMirror
    // DOM update costs. At 10k/100 = 100x blocks, expect <50x ratio.
    expect(ratio).toBeLessThan(50);
  });

  it("numbered list items: typing at beginning", { timeout: 30_000 }, () => {
    const smallEditor = createEditorWithBlocks(SMALL, "numberedListItem");
    const largeEditor = createEditorWithBlocks(LARGE, "numberedListItem");

    // Position 4 = inside the first block's content
    const smallAvg = measureAvgInsertTime(smallEditor, 4);
    const largeAvg = measureAvgInsertTime(largeEditor, 4);
    const ratio = largeAvg / smallAvg;

    // eslint-disable-next-line no-console
    console.log(
      `Numbered list (begin): ${SMALL}=${smallAvg.toFixed(3)}ms, ${LARGE}=${largeAvg.toFixed(3)}ms, ratio=${ratio.toFixed(2)}x`,
    );

    // Typing doesn't change indices, so the plugin exits early once it
    // verifies the first decoration past the changed range still matches.
    // Remaining cost is DecorationSet.map() which is O(n decorations).
    // Ratio appears high (~200x) because the 100-block baseline is very
    // fast for begin-of-doc edits. Absolute time (~32ms) is comparable
    // to end-of-doc (~40ms) — both dominated by DecorationSet.map().
    expect(ratio).toBeLessThan(250);
  });

  it("numbered list items: typing at end", { timeout: 30_000 }, () => {
    const smallEditor = createEditorWithBlocks(SMALL, "numberedListItem");
    const largeEditor = createEditorWithBlocks(LARGE, "numberedListItem");

    const smallAvg = measureAvgInsertTime(
      smallEditor,
      smallEditor._tiptapEditor.view.state.doc.content.size - 2,
    );
    const largeAvg = measureAvgInsertTime(
      largeEditor,
      largeEditor._tiptapEditor.view.state.doc.content.size - 2,
    );
    const ratio = largeAvg / smallAvg;

    // eslint-disable-next-line no-console
    console.log(
      `Numbered list (end): ${SMALL}=${smallAvg.toFixed(3)}ms, ${LARGE}=${largeAvg.toFixed(3)}ms, ratio=${ratio.toFixed(2)}x`,
    );

    // DecorationSet.map() on 10k decorations is O(n) in ProseMirror.
    // Absolute time (~40ms) is comparable to begin (~32ms).
    expect(ratio).toBeLessThan(250);
  });
});
