import { expect, test } from '@playwright/test';
import { createBrowserWorkspace } from './common';

const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';

test('BF4 gives Visual paragraphs headings lists and hard breaks native Learnitive caret behavior', async ({ page }) => {
  const workspaceName = `visual-bf4-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });

  const source = page.getByLabel('LaTeX source editor');
  await source.fill([
    '\\documentclass{article}',
    '\\begin{document}',
    '\\section{Methods}',
    'Existing paragraph.',
    '\\begin{itemize}',
    '\\item First bullet',
    '\\end{itemize}',
    'Hard break target.',
    '\\end{document}',
    '',
  ].join('\n'));
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  await expect(visual).toBeVisible();

  // Heading Enter is owned by Learnitive. The very next keystroke must type into
  // one paragraph directly below the heading, without source-caret recovery.
  const heading = visual.locator('.interactive-visual-heading').filter({ hasText: 'Methods' }).first();
  await heading.click();
  await page.keyboard.press('End');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Native heading paragraph.');
  await expect(visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Native heading paragraph.' })).toBeVisible();

  // Repeated Enter remains inside the same Learnitive view and the final caret is
  // immediately writable.
  const paragraph = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Existing paragraph.' }).first();
  await paragraph.click();
  await page.keyboard.press('End');
  for (let index = 0; index < 5; index += 1) await page.keyboard.press('Enter');
  await page.keyboard.type('After rapid Enter.');
  await expect(visual.locator('.interactive-visual-paragraph').filter({ hasText: 'After rapid Enter.' })).toBeVisible();

  // The caret may be at the END of a word. Native Learnitive list Enter must still
  // create an immediately writable next list item.
  const firstItem = visual.locator('.interactive-visual-list-item').filter({ hasText: 'First bullet' }).first();
  await firstItem.click();
  await page.keyboard.press('End');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Second bullet');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Third bullet');
  await expect(visual.locator('.interactive-visual-list-item').filter({ hasText: 'Second bullet' })).toBeVisible();
  await expect(visual.locator('.interactive-visual-list-item').filter({ hasText: 'Third bullet' })).toBeVisible();

  // Native Learnitive indentation and outdent must leave this as the same logical
  // LaTeX list run rather than manufacturing a second environment.
  await page.keyboard.press('Tab');
  await page.keyboard.press('Shift+Tab');

  // Empty-item Enter exits the list exactly as Learnitive does.
  await page.keyboard.press('End');
  await page.keyboard.press('Enter');
  await page.keyboard.press('Enter');
  await page.keyboard.type('After list exit.');
  await expect(visual.locator('.interactive-visual-paragraph').filter({ hasText: 'After list exit.' })).toBeVisible();

  // Shift+Enter is a real ProseMirror hard_break; typing continues immediately
  // after that node without a synthetic/source-backed caret.
  const hardBreak = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Hard break target.' }).first();
  await hardBreak.click();
  await page.keyboard.press('End');
  await page.keyboard.press('Shift+Enter');
  await page.keyboard.type('After native break.');
  await expect(hardBreak).toContainText('After native break.');

  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\section{Methods}\n\nNative heading paragraph.');
  const finalSource = await source.inputValue();
  expect(finalSource.match(/\\begin\{itemize\}/gu)).toHaveLength(1);
  expect(finalSource.match(/\\end\{itemize\}/gu)).toHaveLength(1);
  expect(finalSource).toContain('\\item First bullet\n\\item Second bullet\n\\item Third bullet');
  await expect(source).toContainText('Hard break target.\\newline{}After native break.');
  await expect(source).not.toContainText('\\par');
});

test('BF4 slash section insertion does not stack synthetic empty paragraphs', async ({ page }) => {
  const workspaceName = `visual-bf4-slash-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\nStart\n\\end{document}\n');
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  const paragraph = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Start' }).first();
  await paragraph.click();
  await page.keyboard.press('End');
  await page.keyboard.press('Enter');
  await page.keyboard.type('/section');
  await expect(page.getByTestId('latex-visual-slash-menu')).toBeVisible();
  await page.keyboard.press('Enter');
  await page.keyboard.type('Section title');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Paragraph after section.');

  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\section{Section title}\n\nParagraph after section.');
  const stableSource = await source.inputValue();
  expect(stableSource).not.toMatch(/\n{4,}\\section\{Section title\}/u);

  for (let index = 0; index < 3; index += 1) {
    await page.getByRole('button', { name: 'Visual' }).click();
    await page.getByRole('button', { name: 'Source' }).click();
    await expect(source).toHaveValue(stableSource);
  }
});

test('BF4.4 slash subsection itemize and enumerate execute at the live Visual paragraph', async ({ page }) => {
  const workspaceName = `visual-bf44-native-slash-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\nStart\n\\end{document}\n');
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  const start = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Start' }).first();
  await start.click();
  await page.keyboard.press('End');
  await page.keyboard.press('Enter');
  await page.keyboard.type('/subsection');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Background');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Body');

  await page.keyboard.press('Enter');
  await page.keyboard.type('/itemize');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Bullet one');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Bullet two');
  await page.keyboard.press('Enter');
  await page.keyboard.press('Enter');

  await page.keyboard.type('/enumerate');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Number one');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Number two');

  await page.getByRole('button', { name: 'Source' }).click();
  const value = await source.inputValue();
  expect(value).toContain('\\subsection{Background}');
  expect(value).toContain('\\begin{itemize}\n\\item Bullet one\n\\item Bullet two\n\\end{itemize}');
  expect(value).toContain('\\begin{enumerate}\n\\item Number one\n\\item Number two\n\\end{enumerate}');
  expect(value).not.toContain('/subsection');
  expect(value).not.toContain('/itemize');
  expect(value).not.toContain('/enumerate');
});

test('BF4 table Enter and boundary navigation stay native while LaTeX remains compilable', async ({ page }) => {
  const workspaceName = `visual-bf4-table-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill([
    '\\documentclass{article}',
    '\\begin{document}',
    '\\begin{tabular}{|l|l|}',
    '\\hline',
    'A & B \\\\',
    '\\hline',
    'C & D \\\\',
    '\\hline',
    '\\end{tabular}',
    '\\end{document}',
    '',
  ].join('\n'));
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  const cells = visual.locator('.interactive-visual-table td');
  await expect(cells).toHaveCount(4);

  const firstCell = cells.first();
  await firstCell.click();
  await page.keyboard.press('End');
  await page.keyboard.press('Enter');
  await page.keyboard.type('Line two');
  await expect(firstCell).toContainText('Line two');

  const lastCell = cells.last();
  await lastCell.click();
  await page.keyboard.press('End');
  await page.keyboard.press('ArrowDown');
  await page.keyboard.type('After table.');
  await expect(visual.locator('.interactive-visual-paragraph').filter({ hasText: 'After table.' })).toBeVisible();

  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\shortstack[l]{A\\\\Line two}');
  await expect(source).toContainText('After table.');
});

test('BF4.2 protected display equation opens the shared MathLive editor and exits into a native paragraph', async ({ page }) => {
  const workspaceName = `visual-bf4-math-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill([
    '\\documentclass{article}',
    '\\begin{document}',
    '\\[',
    '\\frac{d}{dx}\\left(x^2\\right)=2x',
    '\\]',
    '\\end{document}',
    '',
  ].join('\n'));
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  const equation = visual.locator('.interactive-visual-math-display').first();
  const mathField = equation.locator('math-field');
  await expect(equation).toHaveClass(/interactive-visual-math-protected/u);
  await expect(mathField).toBeVisible();
  await expect.poll(() => mathField.evaluate((field) => Boolean((field as HTMLElement & { readOnly?: boolean }).readOnly))).toBe(true);
  await equation.getByRole('button', { name: 'Edit' }).click();
  const editor = page.getByTestId('mathlive-equation-editor');
  await expect(editor).toBeVisible();
  await expect.poll(() => editor.evaluate((node) => node.parentElement === document.body)).toBe(true);
  await expect.poll(() => editor.evaluate((node) => Number.parseInt(getComputedStyle(node).zIndex || '0', 10))).toBeGreaterThanOrEqual(180);
  const editField = editor.locator('math-field');
  await expect.poll(() => editField.evaluate((field) => (field as HTMLElement & { value?: string }).value ?? '')).toContain('\\frac{d}{dx}');
  await editField.evaluate((field) => {
    // Deliberately mutate MathLive's authoritative field without dispatching a
    // React input event. BF4.4 Apply must read field.value directly instead of
    // falling back to stale insertion state such as E=mc^2.
    (field as HTMLElement & { value: string }).value = '\\frac{d}{dx}\\left(x^3\\right)=3x^2';
  });
  await editor.getByRole('button', { name: 'Apply' }).click();
  await expect(editor).toHaveCount(0);
  await expect.poll(() => mathField.evaluate((field) => (field as HTMLElement & { value?: string }).value ?? '')).toContain('\\frac{d}{dx}');
  await expect.poll(() => mathField.evaluate((field) => (field as HTMLElement & { value?: string }).value ?? '')).toContain('3x^2');
  await equation.click();
  await page.keyboard.press('ArrowDown');
  await page.keyboard.type('After equation.');
  await expect(visual.locator('.interactive-visual-paragraph').filter({ hasText: 'After equation.' })).toBeVisible();

  await equation.getByRole('button', { name: 'Edit' }).click();
  const reopenedEditor = page.getByTestId('mathlive-equation-editor');
  await expect(reopenedEditor).toBeVisible();
  const reopenedField = reopenedEditor.locator('math-field');
  await expect.poll(() => reopenedField.evaluate((field) => (field as HTMLElement & { value?: string }).value ?? '')).toContain('x^3');
  await reopenedEditor.getByRole('button', { name: 'Cancel' }).click();

  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\frac{d}{dx}\\left(x^3\\right)=3x^2');
  await expect(source).toContainText('After equation.');
});


test('BF4.1 simple table controls mutate Learnitive structure and keep the LaTeX column spec synchronized', async ({ page }) => {
  const workspaceName = `visual-bf41-table-controls-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill([
    '\\documentclass{article}',
    '\\begin{document}',
    '\\begin{table}[htbp]',
    '\\centering',
    '\\begin{tabular}{|l|l|}',
    '\\hline',
    'A & B \\\\',
    '\\hline',
    'C & D \\\\',
    '\\hline',
    '\\end{tabular}',
    '\\end{table}',
    '\\end{document}',
    '',
  ].join('\n'));
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  const table = visual.locator('.interactive-visual-table').first();
  await expect(table.locator('td')).toHaveCount(4);
  await table.locator('td').first().click();

  await page.getByLabel('Table actions').click();
  await page.getByRole('button', { name: 'Add column right' }).click();
  await expect(table.locator('td')).toHaveCount(6);
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\begin{tabular}{|l|l|l|}');

  await page.getByRole('button', { name: 'Visual' }).click();
  await table.locator('td').first().click();
  await page.getByLabel('Table actions').click();
  await page.getByRole('button', { name: 'Delete column' }).click();
  await expect(table.locator('td')).toHaveCount(4);
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\begin{tabular}{|l|l|}');

  await page.getByRole('button', { name: 'Visual' }).click();
  await table.locator('td').first().click();
  await page.getByLabel('Table actions').click();
  await page.getByRole('button', { name: 'Add row below' }).click();
  await expect(table.locator('td')).toHaveCount(6);
  await page.getByLabel('Table actions').click();
  await page.getByRole('button', { name: 'Delete row' }).click();
  await expect(table.locator('td')).toHaveCount(4);
  await page.keyboard.press('ArrowDown');
  await page.keyboard.type('After table controls.');
  await expect(visual.locator('.interactive-visual-paragraph').filter({ hasText: 'After table controls.' })).toBeVisible();
});

test('BF4.1 protects figure complex-table and raw LaTeX blocks without rewriting their bytes', async ({ page }) => {
  const workspaceName = `visual-bf41-protected-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  const figure = String.raw`\begin{figure}[H]
\centering
\includegraphics[width=.5\linewidth]{result.png}
\caption{Experimental result}
\label{fig:result}
\end{figure}`;
  const complexTable = String.raw`\begin{tabular}{ll}
\toprule
\multicolumn{2}{c}{Result} \\
\midrule
A & B \\
\bottomrule
\end{tabular}`;
  const raw = String.raw`\begin{tikzpicture}
\draw (0,0) -- (1,1);
\end{tikzpicture}`;
  const original = [
    '\\documentclass{article}',
    '\\begin{document}',
    'Before.',
    figure,
    complexTable,
    raw,
    'After.',
    '\\end{document}',
    '',
  ].join('\n\n');
  await source.fill(original);
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  await expect(visual.locator('.interactive-visual-figure')).toHaveCount(1);
  await expect(visual.locator('.interactive-visual-protected-table')).toHaveCount(1);
  await expect(visual.locator('.interactive-visual-raw-block').filter({ hasText: 'tikzpicture' })).toHaveCount(1);
  await expect(visual.locator('.interactive-visual-figure')).toContainText('Experimental result');
  await expect(visual.locator('.interactive-visual-protected-table')).toContainText('Preserved exactly');

  const before = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Before.' }).first();
  await before.click();
  await page.keyboard.press('End');
  await page.keyboard.type(' typed');
  const after = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'After.' }).first();
  await after.click();
  await page.keyboard.press('End');
  await page.keyboard.type(' typed');

  await page.getByRole('button', { name: 'Source' }).click();
  const changed = await source.inputValue();
  expect(changed).toContain(figure);
  expect(changed).toContain(complexTable);
  expect(changed).toContain(raw);
});

test('BF4 native undo synchronizes Source mode even when it returns to the session baseline', async ({ page }) => {
  const workspaceName = `visual-bf4-history-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\nAlpha\n\\end{document}\n');
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Visual' }).click();
  const paragraph = page.locator('.interactive-latex-visual-editor .interactive-visual-paragraph').filter({ hasText: 'Alpha' }).first();
  await paragraph.click();
  await page.keyboard.press('End');
  await page.keyboard.type('X');
  await expect(paragraph).toContainText('AlphaX');

  await page.keyboard.press(process.platform === 'darwin' ? 'Meta+z' : 'Control+z');
  await expect(paragraph).toContainText('Alpha');
  await expect(paragraph).not.toContainText('AlphaX');

  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\nAlpha\n');
  await expect(source).not.toContainText('AlphaX');
});

test('BF4.2 manuscript focus is borderless and the Learnitive gutter handle remains clickable across the hover corridor', async ({ page }) => {
  const workspaceName = `visual-bf42-surface-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\nHover target.\n\\end{document}\n');
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();

  const visual = page.locator('.interactive-latex-visual-editor');
  await visual.focus();
  const focusStyle = await visual.evaluate((element) => {
    const style = getComputedStyle(element);
    return {
      borderTopWidth: style.borderTopWidth,
      outlineStyle: style.outlineStyle,
      outlineWidth: style.outlineWidth,
      boxShadow: style.boxShadow,
    };
  });
  expect(focusStyle.borderTopWidth).toBe('0px');
  expect(focusStyle.outlineStyle === 'none' || focusStyle.outlineWidth === '0px').toBe(true);
  expect(focusStyle.boxShadow === 'none' || focusStyle.boxShadow === '').toBe(true);

  const paragraph = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Hover target.' }).first();
  await paragraph.hover({ position: { x: 2, y: 10 } });
  const handle = page.locator('[data-block-handle]');
  const plus = handle.getByRole('button', { name: 'Add LaTeX block' });
  const drag = handle.getByRole('button', { name: 'Drag LaTeX block to move' });
  await expect(handle).toBeVisible();
  const dragBox = await drag.boundingBox();
  if (!dragBox) throw new Error('Visual drag handle has no bounding box');
  await page.mouse.move(dragBox.x + dragBox.width / 2, dragBox.y + dragBox.height / 2);
  await expect(handle).toBeVisible();
  await expect(drag).toBeVisible();
  await plus.click();
  await expect(page.getByTestId('latex-visual-slash-menu')).toBeVisible();
});

test('BF4.2 standalone and complex structural LaTeX render as protected rectangular cards', async ({ page }) => {
  const workspaceName = `visual-bf42-protected-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill([
    '\\documentclass{article}',
    '\\usepackage{booktabs}',
    '\\begin{document}',
    '\\maketitle',
    '',
    '\\begin{tabular}{ll}',
    '\\toprule',
    'A & B \\\\',
    '\\bottomrule',
    '\\end{tabular}',
    '\\end{document}',
    '',
  ].join('\n'));
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();

  const visual = page.locator('.interactive-latex-visual-editor');
  const rawBlock = visual.locator('.interactive-visual-raw-block[data-visual-kind="raw"]').first();
  await expect(rawBlock).toContainText('LaTeX block');
  await expect(rawBlock.getByRole('button', { name: /Edit LaTeX block source/i })).toBeVisible();
  const rawBorder = await rawBlock.evaluate((element) => getComputedStyle(element).borderTopWidth);
  expect(rawBorder).toBe('1px');

  const complexTable = visual.locator('.interactive-visual-protected-table').first();
  await expect(complexTable).toContainText('Complex LaTeX table');
  await expect(complexTable).toContainText('Custom LaTeX preserved');
  await expect(complexTable.getByRole('button', { name: /Edit Complex LaTeX table source/i })).toBeVisible();

  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\maketitle');
  await expect(source).toContainText('\\toprule');
  await expect(source).toContainText('\\bottomrule');
});

test('BF4.2 native table metadata can seed CSV data and remains centred', async ({ page }) => {
  const workspaceName = `visual-bf42-table-csv-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill([
    '\\documentclass{article}',
    '\\begin{document}',
    '\\begin{table}[htbp]',
    '\\centering',
    '\\begin{tabular}{|l|l|}',
    '\\hline',
    'A & B \\\\',
    '\\hline',
    '\\end{tabular}',
    '\\end{table}',
    '\\end{document}',
    '',
  ].join('\n'));
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();

  const visual = page.locator('.interactive-latex-visual-editor');
  const table = visual.locator('.interactive-visual-table').first();
  await table.locator('td').first().click();
  await expect(table).toBeVisible();
  const alignment = await table.evaluate((element) => getComputedStyle(element).alignSelf);
  expect(alignment).toBe('center');

  await page.locator('.interactive-visual-native-table-toolbar .interactive-visual-table-actions-summary').click();
  await page.getByRole('button', { name: 'Table settings / CSV…' }).click();
  const dialog = page.getByRole('dialog');
  await dialog.getByText('Advanced settings').click();
  await dialog.getByLabel(/CSV/i).fill('Name,Value,Unit\nAlpha,10,ms\nBeta,20,ms');
  await dialog.getByRole('button', { name: /replace table data/i }).click();

  await expect(table.locator('td')).toHaveCount(6);
  await expect(table).toContainText('Alpha');
  await expect(table).toContainText('20');
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\begin{tabular}{|l|l|l|}');
  await expect(source).toContainText('Alpha & 10 & ms');
});

test('BF4.2 Visual figure insertion uses a compiler-safe asset path and provisions graphicx', async ({ page }) => {
  const workspaceName = `visual-bf42-figure-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\nInsert below.\n\\end{document}\n');
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();

  const visual = page.locator('.interactive-latex-visual-editor');
  const paragraph = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Insert below.' }).first();
  await paragraph.hover({ position: { x: 2, y: 10 } });
  await page.getByRole('button', { name: 'Add LaTeX block' }).click();
  await page.getByText('Figure', { exact: true }).click();
  const tinyPng = Buffer.from(
    'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVR4nGP8//8/AwMDEwMDAwMDAwAkBgMB/DXemwAAAABJRU5ErkJggg==',
    'base64',
  );
  await page.getByLabel('Insert Visual figure').setInputFiles({
    name: 'result.png',
    mimeType: 'image/png',
    buffer: tinyPng,
  });

  const figure = visual.locator('.interactive-visual-figure').first();
  await expect(figure).toBeVisible();
  await expect(figure).toContainText('Figure');
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\usepackage{graphicx}');
  await expect(source).toContainText('\\includegraphics[width=0.8\\linewidth]{figures/result.png}');
});

test('BF4.3 projection refresh keeps the Visual caret and list typography stays compact', async ({ page }) => {
  const workspaceName = `visual-bf43-caret-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill([
    '\\documentclass{article}',
    '\\begin{document}',
    'Alpha.',
    '',
    'Beta.',
    '',
    '\\begin{enumerate}',
    '\\item First item',
    '\\item Second item',
    '\\item Third item',
    '\\end{enumerate}',
    '\\end{document}',
    '',
  ].join('\n'));
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  const beta = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Beta.' }).first();
  await beta.click();
  await page.keyboard.press('End');

  // Trigger a semantic/source refresh without asking Visual to navigate.
  await page.getByRole('button', { name: 'Source' }).click();
  await page.getByRole('button', { name: 'Visual' }).click();
  await page.keyboard.type(' still here');
  await expect(visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Beta. still here' })).toBeVisible();
  await expect(visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Alpha. still here' })).toHaveCount(0);

  const items = visual.locator('.interactive-visual-list-item');
  await expect(items).toHaveCount(3);
  const firstBox = await items.nth(0).boundingBox();
  const secondBox = await items.nth(1).boundingBox();
  if (!firstBox || !secondBox) throw new Error('List items are not measurable');
  expect(secondBox.y - (firstBox.y + firstBox.height)).toBeLessThan(14);
});

test("BF4.3 empty paragraph shows the Learnitive slash placeholder", async ({ page }) => {
  const workspaceName = `visual-bf43-placeholder-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\nAlpha\n\\end{document}\n');
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();
  const paragraph = page.locator('.interactive-latex-visual-editor .interactive-visual-paragraph').filter({ hasText: 'Alpha' }).first();
  await paragraph.click();
  await page.keyboard.press('End');
  await page.keyboard.press('Enter');
  await expect(page.locator('.interactive-latex-visual-editor [data-placeholder="Press \'/\' for commands"]')).toBeVisible();
});

test('BF4.3 table trigger is compact and whole-table alignment writes explicit environments', async ({ page }) => {
  const workspaceName = `visual-bf43-table-align-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\n\\begin{tabular}{|l|l|}\n\\hline\nA & B \\\\\n\\hline\n\\end{tabular}\n\\end{document}\n');
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  await visual.locator('td').first().click();
  const trigger = page.locator('.interactive-visual-table-actions-summary');
  await expect(trigger.locator('svg')).toHaveCount(2);
  await trigger.click();
  await page.getByRole('button', { name: 'Center', exact: true }).first().click();
  await expect(visual.locator('table[data-table-align="center"]')).toBeVisible();
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\begin{center}\n\\begin{tabular}');
  await expect(source).toContainText('\\end{tabular}\n\\end{center}');
});

test('BF4.3 immutable top-level LaTeX cannot be deleted in Visual mode', async ({ page }) => {
  const workspaceName = `visual-bf43-immutable-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\n\\maketitle\n\nBody\n\\end{document}\n');
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();
  const protectedBlock = page.locator('.interactive-latex-visual-editor [data-immutable="true"]').first();
  await expect(protectedBlock).toContainText('Protected · Source-only');
  await protectedBlock.click();
  await page.keyboard.press('Backspace');
  await page.keyboard.press('Delete');
  await expect(protectedBlock).toBeVisible();
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\maketitle');
});

test('BF4.3 Learnitive block drag moves PM content and never inserts the old sentinel text', async ({ page }) => {
  const workspaceName = `visual-bf43-drag-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('\\documentclass{article}\n\\begin{document}\nFirst block.\n\nSecond block.\n\\end{document}\n');
  await source.press(saveShortcut);
  await page.getByRole('button', { name: 'Visual' }).click();
  const visual = page.locator('.interactive-latex-visual-editor');
  const first = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'First block.' }).first();
  const second = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Second block.' }).first();
  await first.hover();
  const drag = page.getByRole('button', { name: 'Drag LaTeX block to move' });
  await expect(drag).toBeVisible();
  const secondBox = await second.boundingBox();
  if (!secondBox) throw new Error('Second block is not measurable');
  await drag.dragTo(second, { targetPosition: { x: 12, y: Math.max(2, secondBox.height - 2) } });
  await expect(visual).not.toContainText('interactive-bf4-block');
  await page.getByRole('button', { name: 'Source' }).click();
  const value = await source.inputValue();
  expect(value).not.toContain('interactive-bf4-block');
  expect(value).toContain('First block.');
  expect(value).toContain('Second block.');
  expect(value.indexOf('Second block.')).toBeLessThan(value.indexOf('First block.'));
});

test('BF49 Visual activation reparses canonical source and preserves native math while protecting setup figure and bibliography', async ({ page }) => {
  const workspaceName = `visual-bf49-reparse-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  const fixture = [
    '% complex setup comment',
    '\\documentclass{article}',
    '\\usepackage{amsmath,graphicx}',
    '\\def\\x{{\\mathbf x}}',
    '\\title{Runtime projection}',
    '\\begin{document}',
    '\\maketitle',
    '',
    '\\section{Body}',
    '',
    'Editable paragraph before math.',
    '',
    '\\begin{equation}',
    'E = mc^2',
    '\\end{equation}',
    '',
    '\\begin{figure}[htb]',
    '\\begin{minipage}[b]{1.0\\linewidth}',
    '\\centering',
    '\\centerline{\\includegraphics[width=4cm]{image1}}',
    '\\end{minipage}',
    '\\caption{Complex figure}',
    '\\end{figure}',
    '',
    'References prose remains editable.',
    '',
    '% bibliography setup',
    '\\bibliographystyle{plain}',
    '\\bibliography{refs}',
    '\\end{document}',
    '',
  ].join('\n');
  await source.fill(fixture);
  await source.press(saveShortcut);

  const visualButton = page.getByRole('button', { name: /Visual/ });
  await visualButton.click();
  const visual = page.locator('.interactive-latex-visual-editor');
  await expect(visual.locator('.interactive-visual-document-setup')).toContainText('\\documentclass{article}');
  await expect(visual.locator('.interactive-visual-math-display')).toHaveCount(1);
  await expect(visual.locator('.interactive-visual-figure')).toContainText('Complex figure · Protected LaTeX');
  await expect(visual.locator('[data-visual-kind="bibliography"]')).toContainText('\\bibliography{refs}');

  // Shift every later source range with a native Visual edit. Before BF49 the
  // app marked the old semantic block map as though it belonged to this new
  // source revision; Source -> Visual then misclassified math/figures until a
  // full browser refresh.
  const paragraph = visual.locator('.interactive-visual-paragraph').filter({ hasText: 'Editable paragraph before math.' }).first();
  await paragraph.click();
  await page.keyboard.press('Home');
  await page.keyboard.type('Shifted ');

  for (let cycle = 0; cycle < 3; cycle += 1) {
    await page.getByRole('button', { name: 'Source' }).click();
    await expect(source).toContainText('Shifted Editable paragraph before math.');
    await visualButton.click();
    await expect(visual.locator('.interactive-visual-document-setup')).toContainText('\\def\\x');
    await expect(visual.locator('.interactive-visual-math-display')).toHaveCount(1);
    await expect(visual.locator('.interactive-visual-figure')).toContainText('\\begin{figure}[htb]');
    await expect(visual.locator('[data-visual-kind="bibliography"]')).toContainText('\\bibliographystyle{plain}');
    await expect(visual.locator('.interactive-visual-raw-block').filter({ hasText: '\\begin{equation}' })).toHaveCount(0);
  }
});

test('BF49 TikZ insertion refreshes the active Visual projection immediately', async ({ page }) => {
  const workspaceName = `visual-bf49-tikz-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill([
    '\\documentclass{article}',
    '\\begin{document}',
    'Before TikZ.',
    '\\end{document}',
    '',
  ].join('\n'));
  await source.press(saveShortcut);
  await page.getByRole('button', { name: /Visual/ }).click();

  await page.getByTestId('tikz-visual-editor-toggle').click();
  await expect(page.getByTestId('tikz-visual-editor-dialog')).toBeVisible();
  await expect.poll(() => page.frames().some((frame) => frame.url().includes('/tikz-draw/'))).toBe(true);
  const tikzFrame = page.frames().find((frame) => frame.url().includes('/tikz-draw/'));
  if (!tikzFrame) throw new Error('TikZ editor frame did not load');

  await tikzFrame.evaluate(() => {
    window.parent.postMessage(
      {
        source: 'interactive-tikz-draw',
        type: 'insert',
        payload: {
          tikz: '\\begin{tikzpicture}\n\\draw (0,0) -- (1,1);\n\\end{tikzpicture}',
          packages: [],
          libraries: [],
          images: [],
        },
      },
      window.location.origin,
    );
  });

  await expect(page.getByTestId('tikz-visual-editor-dialog')).toBeHidden();
  const visual = page.locator('.interactive-latex-visual-editor');
  const tikz = visual.locator('[data-visual-kind="tikz"]');
  await expect(tikz).toBeVisible();
  await expect(tikz).toContainText('TikZ · Protected LaTeX');
  await expect(tikz).toContainText('\\draw (0,0) -- (1,1);');
  // No Source -> Visual toggle is performed before the assertions above.
});
