// @vitest-environment jsdom

import { afterEach, describe, expect, it } from 'vitest';
import { NodeSelection, TextSelection } from '@learnitive/prosemirror-editor';
import { parseLatexFile } from '@learnitive/project-semantic-index';
import { projectFilePath } from '@learnitive/project-storage';
import { createLatexVisualEditor, type LatexVisualAction } from '../index';

const editors: Array<ReturnType<typeof createLatexVisualEditor>> = [];
const hosts: HTMLElement[] = [];

function sourceDoc(body: string) {
  // Most interaction tests intentionally omit a meaningful preamble so their
  // first PM child remains the manuscript node under test. Preamble behavior
  // has dedicated fidelity/navigation coverage below.
  return `\\begin{document}\n${body}\n\\end{document}`;
}

function create(source: string, onAction?: (action: LatexVisualAction) => void) {
  const host = document.createElement('div');
  document.body.append(host);
  hosts.push(host);
  let current = source;
  const baseline = source;
  const file = projectFilePath('main.tex');
  const applyTo = (base: string, edits: readonly { from: number; to: number; text: string }[]) => {
    let next = base;
    for (const edit of [...edits].sort((a, b) => b.from - a.from)) {
      next = next.slice(0, edit.from) + edit.text + next.slice(edit.to);
    }
    return next;
  };
  const editor = createLatexVisualEditor({
    host,
    source,
    getSource: () => current,
    blocks: parseLatexFile(file, source).visualBlocks,
    onEdits(edits) {
      current = applyTo(current, edits);
    },
    onNativeEdits(edits) {
      current = applyTo(baseline, edits);
    },
    onAction,
  });
  editors.push(editor);
  return { editor, source: () => current };
}

function press(view: ReturnType<typeof createLatexVisualEditor>['view'], key: string, init: KeyboardEventInit = {}) {
  const event = new KeyboardEvent('keydown', {
    key,
    bubbles: true,
    cancelable: true,
    ...init,
  });
  view.dispatchEvent(event);
  return event.defaultPrevented;
}

function type(view: ReturnType<typeof createLatexVisualEditor>['view'], text: string) {
  view.dispatch(view.state.tr.insertText(text));
}

afterEach(() => {
  for (const editor of editors.splice(0)) editor.destroy();
  for (const host of hosts.splice(0)) host.remove();
});

describe('BF4 Learnitive-native LaTeX Visual editor', () => {
  it('uses the native Learnitive engine when source is supplied', () => {
    const { editor } = create(sourceDoc('Alpha'));
    expect(editor.native).toBe(true);
    expect(editor.view.dom.classList.contains('interactive-latex-visual-editor')).toBe(true);
    expect(editor.view.dom.dataset.learnitiveNativeVisual).toBe('true');
  });

  it('promotes standalone structural LaTeX to a protected rectangular block instead of an inline chip', () => {
    const { editor } = create(sourceDoc('\\maketitle'));
    const block = editor.view.dom.querySelector<HTMLElement>('.interactive-visual-raw-block');
    expect(block).not.toBeNull();
    expect(block?.textContent).toContain('\\maketitle');
    expect(editor.view.dom.querySelector('.interactive-visual-raw-inline')).toBeNull();
  });

  it('renders source-only layout/bibliography structures as immutable labeled boxes', () => {
    const { editor } = create(sourceDoc('\\vfill\\pagebreak\n\n\\bibliographystyle{plain}\n\\bibliography{refs}'));
    const blocks = [...editor.view.dom.querySelectorAll<HTMLElement>('.interactive-visual-raw-block')];
    expect(blocks.length).toBeGreaterThanOrEqual(2);
    expect(blocks.some((block) => block.textContent?.includes('Page layout · Protected LaTeX'))).toBe(true);
    expect(blocks.some((block) => block.textContent?.includes('Bibliography · Protected LaTeX'))).toBe(true);
    expect(blocks.every((block) => block.dataset.immutable === 'true')).toBe(true);
  });

  it('does not render repeated blank source lines as empty visual paragraphs', () => {
    const { editor } = create(sourceDoc('\\section{Introduction}\n\\label{sec:intro}\n\n\n\nParagraph.'));
    const emptyParagraphs = [...editor.view.dom.querySelectorAll<HTMLElement>('.interactive-visual-paragraph')].filter(
      (paragraph) => !paragraph.textContent?.trim(),
    );
    // One trailing editor paragraph is allowed as an insertion affordance; source
    // blank lines themselves must not materialize as several manuscript rows.
    expect(emptyParagraphs.length).toBeLessThanOrEqual(1);
    expect(editor.view.dom.querySelector('h2, h3, h1')?.textContent).toContain('Introduction');
    expect(editor.view.dom.textContent).not.toContain('Label · Protected LaTeX');
  });

  it('renders simple custom text environments as one editable wrapper instead of begin/end LaTeX chips', () => {
    const source = sourceDoc('\\begin{customtext}\nEditable body\n\\end{customtext}');
    const { editor } = create(source);
    const environment = editor.view.dom.querySelector<HTMLElement>('[data-interactive-latex-text-environment]');
    expect(environment).not.toBeNull();
    expect(environment?.textContent).toContain('customtext');
    expect(environment?.textContent).toContain('Editable body');
    expect(environment?.querySelectorAll('[data-interactive-latex-raw-inline]')).toHaveLength(0);
  });

  it('keeps typing forward inside a simple custom text environment', () => {
    const source = sourceDoc('\\begin{customtext}\nEditable body\n\\end{customtext}');
    const host = document.createElement('div');
    document.body.append(host);
    hosts.push(host);
    let current = source;
    const file = projectFilePath('main.tex');
    const applyTo = (base: string, edits: readonly { from: number; to: number; text: string }[]) => {
      let next = base;
      for (const edit of [...edits].sort((a, b) => b.from - a.from)) {
        next = next.slice(0, edit.from) + edit.text + next.slice(edit.to);
      }
      return next;
    };
    const editor = createLatexVisualEditor({
      host,
      source,
      getSource: () => current,
      blocks: parseLatexFile(file, source).visualBlocks,
      onEdits(edits) { current = applyTo(current, edits); },
      onNativeEdits(edits) { current = applyTo(current, edits); return true; },
    });
    editors.push(editor);
    let textPosition: number | undefined;
    editor.view.state.doc.descendants((node, pos) => {
      if (textPosition !== undefined) return false;
      if (node.isText && node.text?.includes('Editable body')) {
        textPosition = pos + node.nodeSize;
        return false;
      }
      return true;
    });
    if (textPosition === undefined) throw new Error('Editable environment text missing');
    editor.view.dispatch(editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, textPosition)));
    editor.view.dispatch(editor.view.state.tr.insertText('A'));
    const firstHead = editor.view.state.selection.head;
    editor.view.dispatch(editor.view.state.tr.insertText('B'));
    expect(editor.view.state.selection.head).toBe(firstHead + 1);
    expect(editor.view.state.doc.textContent).toContain('Editable bodyAB');
    expect(current).toContain('Editable bodyAB');
  });

  it('protects non-simple heading forms instead of silently removing LaTeX options or stars', () => {
    const source = sourceDoc('\\section*{Unnumbered}');
    const { editor } = create(source);
    const native = editor as typeof editor & { currentSource(): string };
    expect(native.currentSource()).toContain('\\section*{Unnumbered}');
    expect(editor.view.dom.querySelector('[data-interactive-latex-raw-block]')).not.toBeNull();
  });

  it('paragraph Enter is native and immediate typing lands in the new paragraph', () => {
    const { editor, source } = create(sourceDoc('Alpha'));
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, 'Beta');

    expect(editor.view.state.doc.childCount).toBe(2);
    expect(editor.view.state.doc.child(0).textContent).toBe('Alpha');
    expect(editor.view.state.doc.child(1).textContent).toBe('Beta');
    expect(source()).toContain('Alpha\n\nBeta');
    expect(source()).not.toContain('\\par');
  });

  it('rapid Enter remains inside one live EditorView and typing lands in the final paragraph', () => {
    const { editor, source } = create(sourceDoc('Alpha'));
    const view = editor.view;
    const first = view.state.doc.firstChild!;
    view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, 1 + first.content.size)));
    for (let index = 0; index < 8; index += 1) expect(press(view, 'Enter')).toBe(true);
    // Empty native paragraphs are editor structure only. They must not turn
    // into an ever-growing run of source blank lines while the user is still
    // navigating to the paragraph they intend to fill.
    expect(source()).toContain('\nAlpha\n\\end{document}');
    expect(source()).not.toMatch(/Alpha\n{3,}/u);
    type(view, 'Omega');
    expect(editor.view).toBe(view);
    expect(view.state.doc.lastChild?.textContent).toBe('Omega');
    expect(source()).toContain('Alpha\n\nOmega');
    expect(source()).not.toMatch(/Alpha\n{3,}Omega/u);
  });

  it('preserves top-level whitespace and source comments exactly across no-op Visual projection', () => {
    const noisy = '\\documentclass{article}\n\\begin{document}\n\n\n% -------------------------------------------------------------------------\n\\maketitle\n\n\n\n\n\\section{Introduction}\n\n\n\nText.\n\n\n\n\\end{document}';
    const { editor } = create(noisy);
    const native = editor as typeof editor & { currentSource(): string };
    const first = native.currentSource();

    expect(first).toBe(noisy);
    expect(editor.view.dom.textContent).not.toContain('-------------------------------------------------------------------------');
    expect(editor.view.dom.querySelector('[data-interactive-source-trivia]')).toBeNull();
    const setup = editor.view.dom.querySelector<HTMLElement>('.interactive-visual-document-setup');
    expect(setup).not.toBeNull();
    expect(setup?.textContent).toContain('Document setup · Protected LaTeX');
    expect(setup?.textContent).toContain('\\documentclass{article}');

    const secondEditor = create(first).editor as typeof editor & { currentSource(): string };
    expect(secondEditor.currentSource()).toBe(noisy);
  });

  it('keeps source-only comment trivia byte-exact while editing adjacent manuscript text', () => {
    const noisy = '\\begin{document}\nAlpha\n% -------------------------------------------------------------------------\n\nBeta\n\\end{document}';
    const { editor, source } = create(noisy);
    const first = editor.view.state.doc.firstChild!;
    expect(first.textContent).toBe('Alpha');
    editor.view.dispatch(
      editor.view.state.tr.insertText(' edited', 1 + first.content.size),
    );

    expect(source()).toBe(
      '\\begin{document}\nAlpha edited\n% -------------------------------------------------------------------------\n\nBeta\n\\end{document}',
    );
    expect(editor.view.dom.textContent).not.toContain('-------------------------------------------------------------------------');
    expect(editor.view.dom.querySelector('[data-interactive-source-trivia]')).toBeNull();
  });

  it('projects preamble, standalone graphics and bibliography as source-navigable protected boxes', () => {
    const source = [
      '\\documentclass{article}',
      '\\usepackage{graphicx}',
      '\\title{Protected setup}',
      '\\begin{document}',
      '% -------------------------------------------------------------------------',
      '\\section{Visible heading}',
      '',
      '{\\includegraphics[width=4cm]{image3}}',
      '',
      '\\bibliographystyle{plain}',
      '\\bibliography{refs}',
      '\\end{document}',
    ].join('\n');
    const navigations: Array<{ id: string; sourceFrom: number; sourceTo: number }> = [];
    const host = document.createElement('div');
    document.body.append(host);
    hosts.push(host);
    const file = projectFilePath('main.tex');
    const editor = createLatexVisualEditor({
      host,
      source,
      getSource: () => source,
      blocks: parseLatexFile(file, source).visualBlocks,
      onEdits() {},
      onNativeEdits() { return true; },
      onNavigateSource(location) { navigations.push(location); },
    });
    editors.push(editor);

    expect(editor.view.dom.textContent).toContain('Visible heading');
    expect(editor.view.dom.textContent).not.toContain('-------------------------------------------------------------------------');
    expect(editor.view.dom.querySelector('.interactive-visual-document-setup')).not.toBeNull();
    expect(editor.view.dom.textContent).toContain('Figure · Protected LaTeX');
    expect(editor.view.dom.textContent).toContain('Bibliography · Protected LaTeX');

    const setup = editor.view.dom.querySelector<HTMLElement>('.interactive-visual-document-setup');
    setup?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    expect(navigations.at(-1)).toEqual({
      id: `preamble:0:${source.indexOf('\\begin{document}')}`,
      sourceFrom: 0,
      sourceTo: source.indexOf('\\begin{document}'),
    });
  });

  it('shows complete protected preamble and complex figure source instead of empty/truncated boxes', () => {
    const source = [
      '% template setup comment',
      '\\documentclass{article}',
      '\\usepackage{spconf,amsmath,graphicx,hyperref}',
      '\\def\\x{{\\mathbf x}}',
      '\\title{Complex title}',
      '% Single address.',
      '\\name{Author(s) Name(s)\\thanks{Thanks to XYZ agency for funding.}}',
      '\\address{Author Affiliation(s)}',
      '% \\twoauthors',
      '%  {A. Author-one}',
      '%  {School A}',
      '\\begin{document}',
      '\\begin{figure}[htb]',
      '\\begin{minipage}[b]{1.0\\linewidth}',
      '  \\centering',
      '  \\centerline{\\includegraphics[width=8.5cm]{image1}}',
      '  \\centerline{(a) Result 1}\\medskip',
      '\\end{minipage}',
      '\\begin{minipage}[b]{.48\\linewidth}',
      '  \\centerline{\\includegraphics[width=4.0cm]{image3}}',
      '  \\centerline{(b) Result 3}\\medskip',
      '\\end{minipage}',
      '\\hfill',
      '\\begin{minipage}[b]{.48\\linewidth}',
      '  \\centerline{\\includegraphics[width=4.0cm]{image4}}',
      '  \\centerline{(c) Result 4}\\medskip',
      '\\end{minipage}',
      '\\caption{Results}',
      '\\label{fig:res}',
      '\\end{figure}',
      '\\end{document}',
    ].join('\n');
    const { editor } = create(source);
    const setup = editor.view.dom.querySelector<HTMLElement>('.interactive-visual-document-setup');
    const preamble = source.slice(0, source.indexOf('\\begin{document}'));
    expect(setup?.querySelector('.interactive-visual-protected-source')?.textContent).toBe(preamble);
    const figure = editor.view.dom.querySelector<HTMLElement>('[data-visual-kind="figure"]');
    const figureFrom = source.indexOf('\\begin{figure}[htb]');
    const figureTo = source.indexOf('\\end{figure}') + '\\end{figure}'.length;
    const rawFigure = source.slice(figureFrom, figureTo);
    expect(figure?.textContent).toContain('Complete LaTeX');
    expect(figure?.querySelector('.interactive-visual-protected-source')?.textContent).toBe(rawFigure);
    expect(figure?.textContent).toContain('image1 · loading preview…');
    expect(figure?.textContent).toContain('image3 · loading preview…');
    expect(figure?.textContent).toContain('image4 · loading preview…');
    expect(figure?.querySelector('.interactive-visual-protected-source')?.textContent.length).toBeGreaterThan(0);
  });

  it('keeps command-looking standalone comments invisible around a protected complex figure', () => {
    const source = [
      '\\begin{document}',
      'Before.',
      '',
      '% Below is an example using \\vspace and \\centerline{\\includegraphics{image1}}.',
      '% \\vfill\\pagebreak',
      '\\begin{figure}[htb]',
      '\\begin{minipage}{.5\\linewidth}',
      '\\includegraphics{image1}',
      '\\end{minipage}',
      '\\caption{Result}',
      '\\end{figure}',
      '% trailing \\vfill and \\pagebreak are comments only',
      '',
      'After.',
      '\\end{document}',
    ].join('\n');
    const { editor } = create(source);
    expect(editor.view.dom.textContent).toContain('Before.');
    expect(editor.view.dom.textContent).toContain('After.');
    expect(editor.view.dom.textContent).not.toContain('Below is an example using');
    expect(editor.view.dom.textContent).not.toContain('trailing \\vfill');
    const figure = editor.view.dom.querySelector<HTMLElement>('[data-visual-kind="figure"]');
    expect(figure?.textContent).toContain('Complex figure · Protected LaTeX');
    expect(figure?.querySelector('.interactive-visual-protected-source')?.textContent).toContain('\\begin{figure}[htb]');
  });

  it('never downgrades a plain raw semantic fallback into Protected LaTeX', () => {
    const source = sourceDoc('Ordinary prose only');
    const file = projectFilePath('main.tex');
    const host = document.createElement('div');
    document.body.append(host);
    hosts.push(host);
    const bodyFrom = source.indexOf('Ordinary prose only');
    const editor = createLatexVisualEditor({
      host,
      source,
      getSource: () => source,
      blocks: [{
        id: `${file}:raw-recovery`,
        kind: 'raw',
        file,
        range: { from: bodyFrom, to: bodyFrom + 'Ordinary prose only'.length },
        text: 'Ordinary prose only',
        reason: 'Synthetic semantic boundary regression fixture.',
      }],
      onEdits() {},
      onNativeEdits() { return true; },
    });
    editors.push(editor);
    expect(editor.view.dom.textContent).toContain('Ordinary prose only');
    expect(editor.view.dom.querySelector('[data-interactive-latex-raw-block]')).toBeNull();
  });

  it('round-trips Visual-authored literal characters back to editable prose across repeated reparses', () => {
    const firstSession = create(sourceDoc('Alpha'));
    const first = firstSession.editor.view.state.doc.firstChild!;
    firstSession.editor.view.dispatch(
      firstSession.editor.view.state.tr.setSelection(
        TextSelection.create(firstSession.editor.view.state.doc, 1 + first.content.size),
      ),
    );
    type(firstSession.editor.view, ' 100% A_B #1 & $ {x} \\ ~ ^');

    const serialized = firstSession.source();
    expect(serialized).toContain(
      String.raw`Alpha 100\% A\_B \#1 \& \$ \{x\} \textbackslash{} \textasciitilde{} \textasciicircum{}`,
    );

    const secondSession = create(serialized);
    expect(secondSession.editor.view.dom.querySelector('[data-interactive-latex-raw-block]')).toBeNull();
    expect(secondSession.editor.view.dom.querySelector('[data-interactive-latex-raw-inline]')).toBeNull();
    expect(secondSession.editor.view.state.doc.firstChild?.textContent).toBe('Alpha 100% A_B #1 & $ {x} \\ ~ ^');

    const reparsed = secondSession.source();
    const thirdSession = create(reparsed);
    expect(thirdSession.editor.view.dom.querySelector('[data-interactive-latex-raw-block]')).toBeNull();
    expect(thirdSession.editor.view.dom.querySelector('[data-interactive-latex-raw-inline]')).toBeNull();
    expect(thirdSession.editor.view.state.doc.firstChild?.textContent).toBe('Alpha 100% A_B #1 & $ {x} \\ ~ ^');
  });

  it('reparses multiple Visual-created paragraphs as editable manuscript text', () => {
    const firstSession = create(sourceDoc('Alpha'));
    const first = firstSession.editor.view.state.doc.firstChild!;
    firstSession.editor.view.dispatch(
      firstSession.editor.view.state.tr.setSelection(
        TextSelection.create(firstSession.editor.view.state.doc, 1 + first.content.size),
      ),
    );
    expect(press(firstSession.editor.view, 'Enter')).toBe(true);
    type(firstSession.editor.view, 'Beta – Unicode 50% A_B');

    const serialized = firstSession.source();
    const secondSession = create(serialized);
    const manuscript = [...Array(secondSession.editor.view.state.doc.childCount)].map(
      (_, index) => secondSession.editor.view.state.doc.child(index),
    );
    expect(manuscript.filter((node) => node.type.name === 'paragraph').map((node) => node.textContent)).toEqual([
      'Alpha',
      'Beta – Unicode 50% A_B',
    ]);
    expect(secondSession.editor.view.dom.querySelector('[data-interactive-latex-raw-block]')).toBeNull();
  });

  it('protects only the unsupported command between editable paragraphs', () => {
    const unsupported = String.raw`\unsupportedcommand{something}`;
    const source = sourceDoc(`Editable paragraph one.\n\n${unsupported}\n\nEditable paragraph two.`);
    const { editor } = create(source);
    const paragraphs = [...editor.view.dom.querySelectorAll<HTMLElement>('.interactive-visual-paragraph')]
      .map((node) => node.textContent)
      .filter(Boolean);
    expect(paragraphs).toContain('Editable paragraph one.');
    expect(paragraphs).toContain('Editable paragraph two.');
    const blocks = [...editor.view.dom.querySelectorAll<HTMLElement>('[data-interactive-latex-raw-block]')];
    expect(blocks).toHaveLength(1);
    expect(blocks[0]?.querySelector('.interactive-visual-protected-source')?.textContent).toBe(unsupported);
  });

  it('keeps source-safe but non-MathLive-editable equations as native equation previews', () => {
    const equation = String.raw`\begin{equation}
E=mc^2 \label{eq:energy}
\end{equation}`;
    const source = sourceDoc(equation);
    const { editor } = create(source);
    expect(editor.view.dom.querySelectorAll('.interactive-visual-math-display')).toHaveLength(1);
    expect(editor.view.dom.querySelector('.interactive-visual-math-display')?.textContent).toContain('E=mc^2');
    expect(editor.view.dom.querySelector('.interactive-visual-raw-block')?.textContent ?? '').not.toContain('\\begin{equation}');
  });

  it('renders tikzpicture as one non-empty source-backed protected block', () => {
    const tikz = String.raw`\begin{tikzpicture}
\draw (0,0) -- (1,1);
\end{tikzpicture}`;
    const source = sourceDoc(tikz);
    const { editor } = create(source);
    const block = editor.view.dom.querySelector<HTMLElement>('[data-visual-kind="tikz"]');
    expect(block).not.toBeNull();
    expect(block?.textContent).toContain('TikZ · Protected LaTeX');
    expect(block?.querySelector('.interactive-visual-protected-source')?.textContent).toBe(tikz);
    expect(block?.querySelector('.interactive-visual-protected-source')?.textContent.length).toBeGreaterThan(0);
  });

  it('heading end Enter creates a writable paragraph immediately below it', () => {
    const { editor, source } = create(sourceDoc('\\section{Methods}'));
    const heading = editor.view.state.doc.firstChild!;
    expect(heading.type.name).toBe('heading');
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + heading.content.size)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, 'First paragraph.');

    expect(editor.view.state.doc.child(0).type.name).toBe('heading');
    expect(editor.view.state.doc.child(1).type.name).toBe('paragraph');
    expect(editor.view.state.doc.child(1).textContent).toBe('First paragraph.');
    expect(source()).toContain('\\section{Methods}\n\nFirst paragraph.');
    expect(source()).not.toMatch(/\\section\{Methods\}\n{4,}/u);
  });

  it('list Enter works from the end of an item and the new item is immediately writable', () => {
    const { editor, source } = create(sourceDoc('\\begin{itemize}\n\\item First item\n\\end{itemize}'));
    const item = editor.view.state.doc.firstChild!;
    expect(item.type.name).toBe('list');
    const paragraph = item.firstChild!;
    const end = 2 + paragraph.content.size;
    editor.view.dispatch(editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, end)));
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, 'Second item');

    expect(editor.view.state.doc.childCount).toBe(2);
    expect(editor.view.state.doc.child(0).type.name).toBe('list');
    expect(editor.view.state.doc.child(1).type.name).toBe('list');
    expect(editor.view.state.doc.child(1).textContent).toBe('Second item');
    expect(source()).toContain('\\item First item\n\\item Second item');
  });

  it('keeps repeated native Enter items in one LaTeX list environment', () => {
    const { editor, source } = create(sourceDoc('\\begin{enumerate}\n\\item First item\n\\end{enumerate}'));
    const first = editor.view.state.doc.firstChild!;
    const firstParagraph = first.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 2 + firstParagraph.content.size)),
    );

    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, 'Second item');
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, 'Third item');

    const serialized = source();
    expect(serialized.match(/\\begin\{enumerate\}/gu)).toHaveLength(1);
    expect(serialized.match(/\\end\{enumerate\}/gu)).toHaveLength(1);
    expect(serialized).toContain('\\item First item\n\\item Second item\n\\item Third item');

    const runIds = Array.from({ length: editor.view.state.doc.childCount }, (_, index) =>
      String(editor.view.state.doc.child(index).attrs.latexListId ?? ''),
    );
    expect(new Set(runIds).size).toBe(1);
    expect(runIds[0]).not.toBe('');
  });

  it('deleting the only native list leaves a writable paragraph instead of an invalid empty document', () => {
    const { editor, source } = create(sourceDoc('\\begin{itemize}\n\\item Only item\n\\end{itemize}'));
    const item = editor.view.state.doc.firstChild!;
    const paragraph = item.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 2 + paragraph.content.size)),
    );
    const deleteList = [...document.querySelectorAll<HTMLButtonElement>('button')].find(
      (button) => button.textContent === 'Delete list',
    );
    expect(deleteList).toBeDefined();
    deleteList?.click();

    expect(editor.view.state.doc.childCount).toBe(1);
    expect(editor.view.state.doc.firstChild?.type.name).toBe('paragraph');
    type(editor.view, 'After deletion');
    expect(editor.view.state.doc.firstChild?.textContent).toBe('After deletion');
    expect(source()).not.toContain('\\begin{itemize}');
    expect(source()).toContain('After deletion');
  });

  it('uses native Learnitive Tab indentation and serializes nested list siblings as one LaTeX environment', () => {
    const { editor, source } = create(sourceDoc('\\begin{itemize}\n\\item Parent\n\\item Child\n\\end{itemize}'));
    const first = editor.view.state.doc.child(0);
    const second = editor.view.state.doc.child(1);
    const secondParagraph = second.firstChild!;
    const secondEnd = first.nodeSize + 2 + secondParagraph.content.size;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, secondEnd)),
    );
    expect(press(editor.view, 'Tab')).toBe(true);

    expect(editor.view.state.doc.childCount).toBe(1);
    expect(editor.view.state.doc.firstChild?.childCount).toBeGreaterThan(1);
    expect(source()).toContain(
      '\\item Parent\n\\begin{itemize}\n\\item Child\n\\end{itemize}',
    );

    expect(press(editor.view, 'Tab', { shiftKey: true })).toBe(true);
    expect(editor.view.state.doc.childCount).toBe(2);
    expect(source()).toContain('\\item Parent\n\\item Child');
    expect(source().match(/\\begin\{itemize\}/gu)).toHaveLength(1);
  });

  it('Enter on an empty list item exits the list and ordinary typing continues outside it', () => {
    const { editor, source } = create(sourceDoc('\\begin{itemize}\n\\item First\n\\item \n\\end{itemize}'));
    const emptyItem = editor.view.state.doc.child(1);
    const emptyParagraphPos = editor.view.state.doc.child(0).nodeSize + 2;
    expect(emptyItem.textContent).toBe('');
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, emptyParagraphPos)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, 'Outside');

    expect(editor.view.state.doc.lastChild?.type.name).toBe('paragraph');
    expect(editor.view.state.doc.lastChild?.textContent).toBe('Outside');
    expect(source()).toContain('\\item First\n\\end{itemize}\n\nOutside');
  });

  it('Shift+Enter creates a native hard_break and immediate typing stays after it', () => {
    const { editor, source } = create(sourceDoc('Alpha'));
    const paragraph = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + paragraph.content.size)),
    );
    expect(press(editor.view, 'Enter', { shiftKey: true })).toBe(true);
    type(editor.view, 'Beta');

    const edited = editor.view.state.doc.firstChild!;
    expect(edited.child(1).type.name).toBe('hard_break');
    expect(edited.textContent).toBe('AlphaBeta');
    expect(source()).toContain('Alpha\\newline{}Beta');
  });

  it('assigns unique native source ids after a Learnitive paragraph split', () => {
    const { editor } = create(sourceDoc('Alpha'));
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);

    const ids = Array.from({ length: editor.view.state.doc.childCount }, (_, index) =>
      String(editor.view.state.doc.child(index).attrs.sourceId ?? ''),
    );
    expect(ids).toHaveLength(2);
    expect(ids[0]).not.toBe('');
    expect(ids[1]).not.toBe('');
    expect(ids[0]).not.toBe(ids[1]);
  });

  it('maps a Visual comment decoration through native typing before the selected text', () => {
    const initial = sourceDoc('Alpha Beta');
    const { editor } = create(initial);
    const native = editor as typeof editor & {
      updateReviewDecorations(items: readonly {
        id: string;
        from: number;
        to: number;
        status: 'open';
        kind: 'comment';
      }[]): void;
    };
    const alphaFrom = initial.indexOf('Alpha');
    native.updateReviewDecorations([
      { id: 'comment-1', from: alphaFrom, to: alphaFrom + 5, status: 'open', kind: 'comment' },
    ]);

    const before = editor.view.dom.querySelector<HTMLElement>('[data-review-thread-id="comment-1"]');
    expect(before?.textContent).toBe('Alpha');

    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1)),
    );
    type(editor.view, 'Intro ');

    const after = editor.view.dom.querySelector<HTMLElement>('[data-review-thread-id="comment-1"]');
    expect(after?.textContent).toBe('Alpha');
    expect(after?.textContent).not.toContain('Intro');
  });

  it('keeps display math protected, exposes its source identity, and ArrowUp creates a writable paragraph above', () => {
    const initial = sourceDoc('\\[\na^2+b^2=c^2\n\\]');
    const { editor, source } = create(initial);
    const equation = editor.view.state.doc.firstChild!;
    expect(equation.type.name).toBe('math_display');
    editor.view.dispatch(
      editor.view.state.tr.setSelection(NodeSelection.create(editor.view.state.doc, 0)),
    );

    const equationDom = editor.view.dom.querySelector<HTMLElement>('.interactive-visual-math-display');
    expect(equationDom?.classList.contains('interactive-visual-math-protected')).toBe(true);
    expect(equationDom?.dataset.sourceId).toBeTruthy();
    expect(press(editor.view, 'ArrowUp')).toBe(true);
    type(editor.view, 'Before equation.');

    expect(editor.view.state.doc.firstChild?.type.name).toBe('paragraph');
    expect(editor.view.state.doc.firstChild?.textContent).toBe('Before equation.');
    expect(source()).toContain('Before equation.\n\n\\[\na^2+b^2=c^2\n\\]');
  });

  it('opens protected display equations through the shared app MathLive action instead of editing the NodeView directly', () => {
    const actions: LatexVisualAction[] = [];
    const { editor } = create(sourceDoc('\\[\na^2+b^2=c^2\n\\]'), (action) => actions.push(action));
    const equationDom = editor.view.dom.querySelector<HTMLElement>('.interactive-visual-math-display');
    const edit = equationDom?.querySelector<HTMLButtonElement>('button');
    expect(edit).not.toBeNull();
    edit?.click();
    const action = actions.find((candidate) => candidate.kind === 'math');
    expect(action?.kind).toBe('math');
    if (action?.kind === 'math') {
      expect(action.value).toBe('a^2+b^2=c^2');
      expect(action.sourceCoordinates).toBe('current');
    }
    const preview = equationDom?.querySelector<HTMLElement>('math-field');
    if (preview) expect((preview as HTMLElement & { readOnly?: boolean }).readOnly).toBe(true);
  });

  it('renders pending deletions as non-editable review widgets instead of hiding the deleted text', () => {
    const initial = sourceDoc('Alpha Beta');
    const { editor } = create(initial);
    const native = editor as typeof editor & {
      updateReviewDecorations(items: readonly {
        id: string;
        from: number;
        to: number;
        status: 'open';
        kind: 'change';
        changeType: 'delete';
        originalText: string;
      }[]): void;
    };
    const betaFrom = initial.indexOf('Beta');
    native.updateReviewDecorations([
      {
        id: 'delete-1',
        from: betaFrom,
        to: betaFrom,
        status: 'open',
        kind: 'change',
        changeType: 'delete',
        originalText: 'Beta',
      },
    ]);
    const deletion = editor.view.dom.querySelector<HTMLElement>('[data-review-thread-id="delete-1"]');
    expect(deletion?.textContent).toBe('Beta');
    expect(deletion?.getAttribute('contenteditable')).toBe('false');
    expect(deletion?.classList.contains('interactive-visual-change-deletion-widget')).toBe(true);
  });

  it('preserves unsupported TikZ source byte-for-byte as a protected native block', () => {
    const source = sourceDoc('\\begin{tikzpicture}\n\\draw (0,0) -- (1,1);\n\\end{tikzpicture}');
    const { editor } = create(source);
    const native = editor as typeof editor & { currentSource(): string };
    expect(native.currentSource()).toContain('\\begin{tikzpicture}\n\\draw (0,0) -- (1,1);\n\\end{tikzpicture}');
    expect(editor.view.dom.querySelector('[data-interactive-latex-raw-block]')).not.toBeNull();
  });

  it('preserves imported emphasis command identity and hard-break spelling', () => {
    const source = sourceDoc('Alpha \\emph{Beta}\\\\Gamma');
    const { editor } = create(source);
    const native = editor as typeof editor & { currentSource(): string };
    expect(native.currentSource()).toContain('Alpha \\emph{Beta}\\\\Gamma');
  });

  it('keeps adjacent same-kind list environments independent while serializing through the native document', () => {
    const source = sourceDoc(
      '\\begin{itemize}\n\\item One\n\\end{itemize}\n\n\\begin{itemize}\n\\item Two\n\\end{itemize}',
    );
    const { editor } = create(source);
    const native = editor as typeof editor & { currentSource(): string };
    const serialized = native.currentSource();
    expect(serialized.match(/\\begin\{itemize\}/gu)).toHaveLength(2);
    expect(serialized.match(/\\end\{itemize\}/gu)).toHaveLength(2);
  });

  it('preserves an unchanged simple table including its wrapper and caption', () => {
    const table = '\\begin{table}[ht]\n\\centering\n\\begin{tabular}{|l|l|}\n\\hline\nA & B \\\\\n\\hline\n\\end{tabular}\n\\caption{Sample}\n\\label{tab:sample}\n\\end{table}';
    const { editor } = create(sourceDoc(table));
    const native = editor as typeof editor & { currentSource(): string };
    expect(native.currentSource()).toContain(table);
  });

  it('protects a booktabs table rather than normalizing its rules', () => {
    const table = '\\begin{tabular}{ll}\n\\toprule\nA & B \\\\\n\\midrule\nC & D \\\\\n\\bottomrule\n\\end{tabular}';
    const { editor } = create(sourceDoc(table));
    const native = editor as typeof editor & { currentSource(): string };
    expect(native.currentSource()).toContain(table);
    expect(editor.view.dom.querySelector('[data-interactive-latex-raw-block]')).not.toBeNull();
  });


  it('protects partial-rule or decorated column specs instead of guessing during native resize', () => {
    for (const spec of ['l|c', '@{}lc', 'p{3cm}l']) {
      const table = `\\begin{tabular}{${spec}}\nA & B \\\\\n\\end{tabular}`;
      const { editor } = create(sourceDoc(table));
      expect(editor.view.dom.querySelector('[data-interactive-latex-raw-block]')).not.toBeNull();
      const native = editor as typeof editor & { currentSource(): string };
      expect(native.currentSource()).toContain(table);
    }
  });

  it('keeps native table Enter while serializing cell line breaks as compilable LaTeX', () => {
    const table = String.raw`\begin{tabular}{|l|l|}
\hline
A & B \\
\hline
\end{tabular}`;
    const { editor, source } = create(sourceDoc(table));
    let firstCellPos: number | undefined;
    editor.view.state.doc.descendants((node, pos) => {
      if (firstCellPos === undefined && node.type.name === 'table_cell') {
        firstCellPos = pos;
        return false;
      }
      return firstCellPos === undefined;
    });
    expect(firstCellPos).toBeTypeOf('number');
    const cell = editor.view.state.doc.nodeAt(firstCellPos!);
    expect(cell?.type.name).toBe('table_cell');
    editor.view.dispatch(
      editor.view.state.tr.setSelection(
        TextSelection.create(editor.view.state.doc, firstCellPos! + 1 + (cell?.content.size ?? 0)),
      ),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    expect(source()).toContain('\\shortstack[l]{A\\\\}');
    type(editor.view, 'Line two');

    expect(source()).toContain('\\shortstack[l]{A\\\\Line two}');
    expect(editor.view.state.selection.$from.parent.type.name).toBe('table_cell');
  });


  it('resizes the LaTeX column specification after native Learnitive column deletion and insertion', () => {
    const table = String.raw`\begin{tabular}{|l|l|}
\hline
A & B \\
\hline
C & D \\
\hline
\end{tabular}`;
    const { editor, source } = create(sourceDoc(table));
    let firstCellPos: number | undefined;
    editor.view.state.doc.descendants((node, pos) => {
      if (firstCellPos === undefined && node.type.name === 'table_cell') {
        firstCellPos = pos;
        return false;
      }
      return firstCellPos === undefined;
    });
    expect(firstCellPos).toBeTypeOf('number');
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, firstCellPos! + 1)),
    );

    const deleteColumn = [...document.querySelectorAll<HTMLButtonElement>('button')].find(
      (button) => button.textContent === 'Delete column',
    );
    expect(deleteColumn).toBeDefined();
    deleteColumn?.click();
    expect(source()).toContain('\\begin{tabular}{|l|}');
    expect(editor.view.state.doc.firstChild?.firstChild?.childCount).toBe(1);

    const remainingCell = editor.view.state.doc.firstChild?.firstChild?.firstChild;
    expect(remainingCell?.type.name).toBe('table_cell');
    let remainingCellPos: number | undefined;
    editor.view.state.doc.descendants((node, pos) => {
      if (remainingCellPos === undefined && node.type.name === 'table_cell') {
        remainingCellPos = pos;
        return false;
      }
      return remainingCellPos === undefined;
    });
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, remainingCellPos! + 1)),
    );

    const addColumnRight = [...document.querySelectorAll<HTMLButtonElement>('button')].find(
      (button) => button.textContent === 'Add column right',
    );
    expect(addColumnRight).toBeDefined();
    addColumnRight?.click();
    expect(source()).toContain('\\begin{tabular}{|l|l|}');
    expect(editor.view.state.doc.firstChild?.firstChild?.childCount).toBe(2);
  });

  it('serializes safe native table alignment changes instead of restoring the untouched original table', () => {
    const table = String.raw`\begin{tabular}{|l|l|}
\hline
A & B \\
\hline
\end{tabular}`;
    const { editor, source } = create(sourceDoc(table));
    let firstCellPos: number | undefined;
    editor.view.state.doc.descendants((node, pos) => {
      if (firstCellPos === undefined && node.type.name === 'table_cell') {
        firstCellPos = pos;
        return false;
      }
      return firstCellPos === undefined;
    });
    expect(firstCellPos).toBeTypeOf('number');
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, firstCellPos! + 1)),
    );

    const columnLabels = [...document.querySelectorAll<HTMLElement>('.interactive-visual-table-menu-label')];
    const columnLabel = columnLabels.find((label) => label.textContent === 'Column alignment');
    const columnMenu = columnLabel?.parentElement;
    const alignCentre = columnMenu
      ? [...columnMenu.querySelectorAll<HTMLButtonElement>('button')].filter((button) => !button.dataset.tableAlignmentOption).find((button) => button.textContent === 'Center')
      : undefined;
    expect(alignCentre).toBeDefined();
    alignCentre?.click();
    expect(source()).toContain('\\begin{tabular}{|c|l|}');
  });

  it('reconciles an external Source-mode replacement into the same EditorView', () => {
    const initial = sourceDoc('Alpha');
    const replacement = sourceDoc('Beta');
    const host = document.createElement('div');
    document.body.append(host);
    hosts.push(host);
    let current = initial;
    const file = projectFilePath('main.tex');
    const editor = createLatexVisualEditor({
      host,
      source: initial,
      getSource: () => current,
      blocks: parseLatexFile(file, initial).visualBlocks,
      onEdits(edits) {
        for (const edit of [...edits].sort((a, b) => b.from - a.from)) {
          current = current.slice(0, edit.from) + edit.text + current.slice(edit.to);
        }
      },
    });
    editors.push(editor);
    const view = editor.view;
    current = replacement;
    expect(editor.replaceProjection(parseLatexFile(file, replacement).visualBlocks)).toBe(true);
    expect(editor.view).toBe(view);
    expect(view.state.doc.textContent).toBe('Beta');
  });

  it('native undo back to the Visual baseline still synchronizes the canonical LaTeX source', () => {
    const baseline = sourceDoc('Alpha');
    const { editor, source } = create(baseline);
    const paragraph = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + paragraph.content.size)),
    );
    type(editor.view, 'X');
    expect(source()).toContain('AlphaX');

    expect(press(editor.view, 'z', { ctrlKey: true })).toBe(true);
    expect(source()).toBe(baseline);
    expect(editor.view.state.doc.textContent).toBe('Alpha');
  });

  it('marks native history replays so canonical source sync can avoid recording undo-of-undo', () => {
    const host = document.createElement('div');
    document.body.append(host);
    hosts.push(host);
    let current = sourceDoc('Alpha');
    const file = projectFilePath('main.tex');
    const historyReplay: boolean[] = [];
    const editor = createLatexVisualEditor({
      host,
      source: current,
      getSource: () => current,
      blocks: parseLatexFile(file, current).visualBlocks,
      onEdits() {},
      onNativeEdits(edits, meta) {
        historyReplay.push(meta.historyReplay);
        for (const edit of [...edits].sort((a, b) => b.from - a.from)) {
          current = current.slice(0, edit.from) + edit.text + current.slice(edit.to);
        }
      },
    });
    editors.push(editor);
    editor.view.dispatch(editor.view.state.tr.insertText('X').setMeta('history$', { bf4Test: true }));
    expect(historyReplay.at(-1)).toBe(true);
  });

});

describe('BF4.3 Visual stability and Learnitive parity', () => {
  it('preserves the live caret across projection refresh instead of jumping to document start', () => {
    const source = sourceDoc('Alpha\n\nBeta');
    const { editor } = create(source);
    const second = editor.view.state.doc.child(1)!;
    const secondPos = editor.view.state.doc.child(0)!.nodeSize + 1 + second.content.size;
    editor.view.dispatch(editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, secondPos)));

    expect(editor.replaceProjection(parseLatexFile(projectFilePath('main.tex'), source).visualBlocks)).toBe(true);
    type(editor.view, 'X');

    expect(editor.view.state.doc.child(0).textContent).toBe('Alpha');
    expect(editor.view.state.doc.child(1).textContent).toBe('BetaX');
  });

  it('never synthesizes Learnitive review metadata into Visual-serialized LaTeX', () => {
    const source = sourceDoc('Alpha');
    const { editor } = create(source);
    const native = editor as typeof editor & { currentSource(): string };
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)));
    type(editor.view, 'X');
    expect(native.currentSource()).toContain('AlphaX');
    expect(native.currentSource()).not.toContain('LEARNITIVE-REVIEW');
  });

  it("shows Press '/' for commands on the active empty paragraph", () => {
    const { editor } = create(sourceDoc('Alpha'));
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)));
    expect(press(editor.view, 'Enter')).toBe(true);
    expect(editor.view.dom.querySelector('[data-placeholder]')?.getAttribute('data-placeholder')).toBe("Press '/' for commands");
  });

  it('does not allow immutable document-shell LaTeX to be deleted from Visual mode', () => {
    const { editor, source } = create(sourceDoc('\\maketitle\n\nBody'));
    let protectedPos: number | undefined;
    editor.view.state.doc.descendants((node, pos) => {
      if (protectedPos === undefined && node.type.name === 'latex_raw_block' && node.attrs.immutable === true) {
        protectedPos = pos;
        return false;
      }
      return protectedPos === undefined;
    });
    expect(protectedPos).toBeTypeOf('number');
    editor.view.dispatch(editor.view.state.tr.setSelection(NodeSelection.create(editor.view.state.doc, protectedPos!)));
    editor.view.dispatch(editor.view.state.tr.deleteSelection());
    expect(source()).toContain('\\maketitle');
    expect(editor.view.dom.querySelector('[data-immutable="true"]')).not.toBeNull();
  });

  it('uses a compact table icon/chevron trigger and serializes whole-table centering', () => {
    const table = String.raw`\begin{tabular}{|l|l|}
\hline
A & B \\
\hline
\end{tabular}`;
    const { editor, source } = create(sourceDoc(table));
    let cellPos: number | undefined;
    editor.view.state.doc.descendants((node, pos) => {
      if (cellPos === undefined && node.type.name === 'table_cell') {
        cellPos = pos;
        return false;
      }
      return cellPos === undefined;
    });
    editor.view.dispatch(editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, cellPos! + 1)));
    const trigger = document.querySelector<HTMLElement>('.interactive-visual-table-actions-summary');
    expect(trigger?.querySelectorAll('svg')).toHaveLength(2);
    const center = document.querySelector<HTMLButtonElement>('button[data-table-alignment-option="true"]:nth-of-type(3)')
      ?? [...document.querySelectorAll<HTMLButtonElement>('button[data-table-alignment-option="true"]')].find((button) => button.textContent === 'Center');
    expect(center).toBeDefined();
    center?.click();
    expect(source()).toContain('\\begin{center}\n\\begin{tabular}');
    expect(source()).toContain('\\end{tabular}\n\\end{center}');
    expect(editor.view.dom.querySelector('table[data-table-align="center"]')).not.toBeNull();
  });

  it('pastes spreadsheet tabular data into the native table and expands it', () => {
    const table = String.raw`\begin{tabular}{|l|l|}
\hline
A & B \\
\hline
\end{tabular}`;
    const { editor, source } = create(sourceDoc(table));
    let cellPos: number | undefined;
    editor.view.state.doc.descendants((node, pos) => {
      if (cellPos === undefined && node.type.name === 'table_cell') {
        cellPos = pos;
        return false;
      }
      return cellPos === undefined;
    });
    editor.view.dispatch(editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, cellPos! + 1)));
    const event = new Event('paste', { bubbles: true, cancelable: true }) as ClipboardEvent;
    Object.defineProperty(event, 'clipboardData', {
      value: { getData: (type_: string) => type_ === 'text/plain' ? 'Name\tValue\nGPU\t1024' : '' },
    });
    editor.view.dom.dispatchEvent(event);
    expect(event.defaultPrevented).toBe(true);
    expect(source()).toContain('Name & Value');
    expect(source()).toContain('GPU & 1024');
  });

  it('spreadsheet paste preserves untouched rich cells and synchronizes the expanded column spec', () => {
    const table = String.raw`\begin{tabular}{|l|l|}
\hline
\textbf{Keep} & Paste here \\
\hline
\end{tabular}`;
    const { editor, source } = create(sourceDoc(table));
    const cellPositions: number[] = [];
    editor.view.state.doc.descendants((node, pos) => {
      if (node.type.name === 'table_cell') cellPositions.push(pos);
      return true;
    });
    expect(cellPositions).toHaveLength(2);
    editor.view.dispatch(editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, cellPositions[1]! + 1)));
    const event = new Event('paste', { bubbles: true, cancelable: true }) as ClipboardEvent;
    Object.defineProperty(event, 'clipboardData', {
      value: { getData: (type_: string) => type_ === 'text/plain' ? 'GPU\t1024' : '' },
    });
    editor.view.dom.dispatchEvent(event);

    expect(event.defaultPrevented).toBe(true);
    expect(source()).toContain('\\textbf{Keep}');
    expect(source()).toContain('\\begin{tabular}{|l|l|l|}');
    expect(source()).toMatch(/\\textbf\{Keep\}\s*&\s*GPU\s*&\s*1024/u);
  });

});

describe('BF4.4 exact-caret structure and equation fidelity', () => {
  it('transforms the current slash paragraph into a section at the live PM caret', () => {
    const { editor, source } = create(sourceDoc('Alpha'));
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, '/section');

    expect(editor.executeNativeSlashCommand('section')).toBe(true);
    type(editor.view, 'Methods');

    expect(editor.view.state.doc.child(0).textContent).toBe('Alpha');
    expect(editor.view.state.doc.child(1).type.name).toBe('heading');
    expect(editor.view.state.doc.child(1).textContent).toBe('Methods');
    expect(source()).toContain('Alpha\n\n\\section{Methods}');
    expect(source()).not.toContain('/section');
  });

  it('transforms the current slash paragraph into a subsection at the live PM caret', () => {
    const { editor, source } = create(sourceDoc('Alpha'));
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, '/subsection');

    expect(editor.executeNativeSlashCommand('subsection')).toBe(true);
    type(editor.view, 'Background');

    expect(editor.view.state.doc.child(0).textContent).toBe('Alpha');
    expect(editor.view.state.doc.child(1).type.name).toBe('heading');
    expect(editor.view.state.doc.child(1).attrs.level).toBe(3);
    expect(editor.view.state.doc.child(1).textContent).toBe('Background');
    expect(source()).toContain('Alpha\n\n\\subsection{Background}');
    expect(source()).not.toContain('/subsection');
  });

  it('preserves both paragraph fragments when a block slash command executes in the middle of prose', () => {
    const { editor, source } = create(sourceDoc('Before '));
    const paragraph = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + paragraph.content.size)),
    );
    type(editor.view, '/section');
    type(editor.view, ' after');
    // Move the live PM caret back to the end of the slash token, leaving the
    // trailing prose in the same paragraph. updateSlash is selection-driven.
    const slashEnd = 1 + 'Before /section'.length;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, slashEnd)),
    );

    expect(editor.executeNativeSlashCommand('section')).toBe(true);
    type(editor.view, 'Inserted');

    expect(editor.view.state.doc.child(0).textContent).toBe('Before ');
    expect(editor.view.state.doc.child(1).type.name).toBe('heading');
    expect(editor.view.state.doc.child(1).textContent).toBe('Inserted');
    expect(editor.view.state.doc.child(2).textContent).toBe(' after');
    expect(source()).toContain('Before \n\n\\section{Inserted}\n\n after');
    expect(source()).not.toContain('/section');
  });

  it('splits surrounding prose around a block slash command instead of source-offset inserting later', () => {
    const { editor, source } = create(sourceDoc('Before /section'));
    const paragraph = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + paragraph.content.size)),
    );

    expect(editor.executeNativeSlashCommand('section')).toBe(true);
    type(editor.view, 'Inserted');

    expect(editor.view.state.doc.child(0).type.name).toBe('paragraph');
    expect(editor.view.state.doc.child(0).textContent).toBe('Before ');
    expect(editor.view.state.doc.child(1).type.name).toBe('heading');
    expect(editor.view.state.doc.child(1).textContent).toBe('Inserted');
    expect(source()).toContain('\\section{Inserted}');
    expect(source()).not.toContain('/section');
  });

  it('keeps native slash serialization stable when the generated source is reparsed', () => {
    const firstSession = create(sourceDoc('Alpha'));
    const first = firstSession.editor.view.state.doc.firstChild!;
    firstSession.editor.view.dispatch(
      firstSession.editor.view.state.tr.setSelection(
        TextSelection.create(firstSession.editor.view.state.doc, 1 + first.content.size),
      ),
    );
    expect(press(firstSession.editor.view, 'Enter')).toBe(true);
    type(firstSession.editor.view, '/subsection');
    expect(firstSession.editor.executeNativeSlashCommand('subsection')).toBe(true);
    type(firstSession.editor.view, 'Stable');
    const stable = firstSession.source();

    const secondSession = create(stable);
    const secondEditor = secondSession.editor as typeof firstSession.editor & { currentSource(): string };
    expect(secondEditor.currentSource()).toBe(stable);
    const thirdSession = create(secondEditor.currentSource());
    const thirdEditor = thirdSession.editor as typeof firstSession.editor & { currentSource(): string };
    expect(thirdEditor.currentSource()).toBe(stable);
  });

  it('turns the current slash paragraph into one native itemize/enumerate run', () => {
    for (const [command, environment] of [
      ['bullet-list', 'itemize'],
      ['numbered-list', 'enumerate'],
    ] as const) {
      const { editor, source } = create(sourceDoc('Alpha'));
      const first = editor.view.state.doc.firstChild!;
      editor.view.dispatch(
        editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)),
      );
      expect(press(editor.view, 'Enter')).toBe(true);
      type(editor.view, command === 'bullet-list' ? '/itemize' : '/enumerate');
      expect(editor.executeNativeSlashCommand(command)).toBe(true);
      type(editor.view, 'First');
      expect(press(editor.view, 'Enter')).toBe(true);
      type(editor.view, 'Second');

      const serialized = source();
      expect(serialized.match(new RegExp(`\\\\begin\\{${environment}\\}`, 'gu'))).toHaveLength(1);
      expect(serialized.match(new RegExp(`\\\\end\\{${environment}\\}`, 'gu'))).toHaveLength(1);
      expect(serialized).toContain('\\item First\n\\item Second');
    }
  });

  it('inserts block structures between live PM paragraph fragments', () => {
    const actions: LatexVisualAction[] = [];
    const { editor, source } = create(sourceDoc('Before /equation'), (action) => actions.push(action));
    const paragraph = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + paragraph.content.size)),
    );

    expect(editor.executeNativeSlashCommand('equation')).toBe(true);
    expect(editor.view.state.doc.child(0).textContent).toBe('Before ');
    expect(editor.view.state.doc.child(1).type.name).toBe('math_display');
    expect(source()).toContain('\\begin{equation}\nE=mc^2\n\\end{equation}');
    expect(source()).not.toContain('/equation');
    expect(actions.some((action) => action.kind === 'math' && action.value === 'E=mc^2')).toBe(true);
  });

  it('inserts a Learnitive table at the slash paragraph and centers it by default', () => {
    const { editor, source } = create(sourceDoc('Alpha'));
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, '/table');
    expect(editor.insertNativeTableAtSlash(2, 3)).toBe(true);

    expect(editor.view.state.doc.child(1).type.name).toBe('table');
    expect(source()).toContain('\\begin{center}\n\\begin{tabular}{|l|l|l|}');
    expect(source()).toContain('\\end{tabular}\n\\end{center}');
  });

  it('inserts a table at the live slash position without moving it to the following paragraph', () => {
    const { editor, source } = create(sourceDoc('Before /table'));
    const paragraph = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + paragraph.content.size)),
    );

    expect(editor.insertNativeTableAtSlash(2, 2)).toBe(true);
    expect(editor.view.state.doc.child(0).textContent).toBe('Before ');
    expect(editor.view.state.doc.child(1).type.name).toBe('table');
    expect(source()).toContain('\\begin{center}\n\\begin{tabular}{|l|l|}');
    expect(source()).not.toContain('/table');
  });

  it('hydrates an existing derivative edit action from protected PM LaTeX and preserves no-op source', () => {
    const derivative = '\\frac{d}{dx}\\left(x^2\\right)=2x';
    const actions: LatexVisualAction[] = [];
    const { editor, source } = create(sourceDoc(`\\[\n${derivative}\n\\]`), (action) => actions.push(action));
    const edit = editor.view.dom.querySelector<HTMLButtonElement>('[aria-label="Edit equation"]');
    expect(edit).not.toBeNull();
    edit?.click();

    const action = actions.find((candidate): candidate is Extract<LatexVisualAction, { kind: 'math' }> => candidate.kind === 'math');
    expect(action).toBeDefined();
    expect(action?.value).toContain('\\frac{d}{dx}');
    expect(action?.editable).toBe(true);
    const before = source();
    if (!action) throw new Error('Expected equation action');
    editor.updateMathValue(action.id, action.value);
    expect(source()).toBe(before);

    editor.updateMathValue(action.id, '\\frac{d}{dx}x^3=3x^2');
    expect(source()).toContain('\\frac{d}{dx}x^3=3x^2');
  });

  it('keeps advanced derivative/fraction/integral LaTeX editable from protected PM nodes', () => {
    const fixtures = [
      '\\frac{d}{dx}f(x)',
      '\\frac{\\partial^2 u}{\\partial x^2}',
      '\\left.\\frac{d}{dt}f(t)\\right|_{t=0}',
      '\\nabla \\cdot \\mathbf{E} = \\frac{\\rho}{\\varepsilon_0}',
      '\\sum_{n=1}^{\\infty}\\frac{1}{n^2}',
      '\\int_0^\\infty e^{-x^2}\\,dx',
      '\\mathbf{A}^{-1}\\mathbf{b}',
      '\\begin{pmatrix}1 & 0 \\\\ 0 & 1\\end{pmatrix}',
    ];
    for (const fixture of fixtures) {
      const actions: LatexVisualAction[] = [];
      const { editor, source } = create(sourceDoc(`\\[\n${fixture}\n\\]`), (action) => actions.push(action));
      editor.view.dom.querySelector<HTMLButtonElement>('[aria-label="Edit equation"]')?.click();
      const action = actions.find((candidate): candidate is Extract<LatexVisualAction, { kind: 'math' }> => candidate.kind === 'math');
      expect(action?.value).toContain(fixture);
      expect(action?.editable).toBe(true);
      const before = source();
      if (action) editor.updateMathValue(action.id, action.value);
      expect(source()).toBe(before);
    }
  });

  it('creates bracketed display math at the live slash paragraph', () => {
    const actions: LatexVisualAction[] = [];
    const { editor, source } = create(sourceDoc('Alpha'), (action) => actions.push(action));
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, '/display-math');
    expect(editor.executeNativeSlashCommand('display-math')).toBe(true);

    const mathAction = actions
      .filter((candidate): candidate is Extract<LatexVisualAction, { kind: 'math' }> => candidate.kind === 'math')
      .at(-1);
    expect(mathAction?.value).toBe('E=mc^2');
    expect(source()).toContain('\\[\nE=mc^2\n\\]');
    expect(source()).not.toContain('/display-math');
  });

  it('creates display equations at the live slash paragraph and opens the PM-backed equation action', () => {
    const actions: LatexVisualAction[] = [];
    const { editor, source } = create(sourceDoc('Alpha'), (action) => actions.push(action));
    const first = editor.view.state.doc.firstChild!;
    editor.view.dispatch(
      editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, 1 + first.content.size)),
    );
    expect(press(editor.view, 'Enter')).toBe(true);
    type(editor.view, '/equation');
    expect(editor.executeNativeSlashCommand('equation')).toBe(true);

    const mathAction = actions
      .filter((candidate): candidate is Extract<LatexVisualAction, { kind: 'math' }> => candidate.kind === 'math')
      .at(-1);
    expect(mathAction?.value).toBe('E=mc^2');
    expect(source()).toContain('\\begin{equation}\nE=mc^2\n\\end{equation}');
  });
});
