import { expect, type Locator, type Page, test } from '@playwright/test';
import { createBrowserWorkspace, pressAppShortcut } from './common';

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

const PNG_1X1 = Buffer.from(
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2nGQAAAAASUVORK5CYII=',
  'base64',
);

async function createTextFile(page: Page, fileName: string, content: string) {
  const explorer = page.getByTestId('interactive-file-explorer');
  await explorer.getByRole('button', { name: 'New File' }).click();
  const dialog = page.getByRole('dialog', { name: 'Create Project File' });
  await dialog.getByLabel('File name').fill(fileName);
  await dialog.getByRole('button', { name: 'Create' }).click();
  const source = page.getByLabel('LaTeX source editor');
  await expect(source).toBeVisible();
  await source.fill(content);
  await source.press(saveShortcut);
  await expect(page.getByText('Saved', { exact: true })).toBeVisible();
}

async function clickSourceToolbar(page: Page, label: string) {
  let button = page.getByRole('button', { name: label, exact: true });
  if ((await button.count()) === 0) {
    await page.getByRole('button', { name: 'More editor commands' }).click();
    button = page.getByRole('menu').getByRole('button', { name: label, exact: true });
  }
  await expect(button).toBeVisible();
  await button.click();
}

async function selectVisualText(page: Page, selector: string, needle: string) {
  await page.locator(selector).evaluate((element, text) => {
    const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
    let node = walker.nextNode() as Text | null;
    while (node) {
      const index = node.data.indexOf(text);
      if (index >= 0) {
        const range = document.createRange();
        range.setStart(node, index);
        range.setEnd(node, index + text.length);
        const selection = window.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
        document.dispatchEvent(new Event('selectionchange'));
        return;
      }
      node = walker.nextNode() as Text | null;
    }
    throw new Error(`Unable to select ${text}`);
  }, needle);
}

async function openVisualSlash(page: Page, paragraphText: string, query: string) {
  const visual = page.locator('.interactive-latex-visual-editor');
  const paragraph = visual
    .locator('.interactive-visual-paragraph')
    .filter({ hasText: paragraphText })
    .first();
  await expect(paragraph).toBeVisible();
  await paragraph.click();
  await paragraph.press('/');
  if (query) await page.keyboard.type(query);
  const slash = page.getByTestId('latex-visual-slash-menu');
  await expect(slash).toBeVisible();
  return slash;
}

async function setMathValue(editor: Locator, value: string) {
  const field = editor.locator('math-field');
  await expect(field).toBeVisible();
  await field.evaluate((element, nextValue) => {
    const math = element as HTMLElement & { value: string };
    math.value = nextValue;
    math.dispatchEvent(new Event('input', { bubbles: true }));
  }, value);
}

async function compileCurrentProject(page: Page) {
  await page.getByRole('button', { name: 'Compiler settings' }).click();
  await expect(page.getByText('Browser compiler runtime')).toBeVisible();
  await page.keyboard.press('Escape');
  await page.getByRole('button', { name: 'Compile', exact: true }).click();
  await expect(page.getByTestId('interactive-pdf-preview').locator('canvas')).toBeVisible({ timeout: 180_000 });
  await expect(page.getByTestId('interactive-pdf-preview').locator('canvas')).toBeVisible({ timeout: 60_000 });
}

async function currentCursorOffset(page: Page) {
  return page.locator('[data-latex-codemirror]').getAttribute('data-cursor-offset');
}

async function expectCursorOffset(page: Page, expected: string) {
  await expect(page.locator('[data-latex-codemirror]')).toHaveAttribute('data-cursor-offset', expected);
}

test('R14.20 certifies 100 complete main → chapter → style → bib → main file-switch cycles', async ({ page }) => {
  test.setTimeout(900_000);
  const workspaceName = `cert-files-r14-20-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  await source.fill('MAIN-CONTENT\n');
  await source.press(saveShortcut);
  await createTextFile(page, 'chapter.tex', 'CHAPTER-CONTENT\n');
  await createTextFile(page, 'style.sty', '% STYLE-CONTENT\n');
  await createTextFile(page, 'refs.bib', '@misc{r1420,title={BIB-CONTENT}}\n');

  const sequence = [
    { name: /^main\.tex$/, marker: 'MAIN-CONTENT' },
    { name: /^chapter\.tex$/, marker: 'CHAPTER-CONTENT' },
    { name: /^style\.sty$/, marker: 'STYLE-CONTENT' },
    { name: /^refs\.bib$/, marker: 'BIB-CONTENT' },
    { name: /^main\.tex$/, marker: 'MAIN-CONTENT' },
  ] as const;

  for (let cycle = 0; cycle < 100; cycle += 1) {
    for (const target of sequence) {
      await page.getByRole('link', { name: target.name }).click();
      const active = page.getByLabel('LaTeX source editor');
      await expect(active).toBeVisible();
      await expect(active).toContainText(target.marker);
      for (const other of ['MAIN-CONTENT', 'CHAPTER-CONTENT', 'STYLE-CONTENT', 'BIB-CONTENT']) {
        if (other !== target.marker) await expect(active).not.toContainText(other);
      }
    }
  }
});

test('R14.20 keeps the Source caret stable through autosave, refresh, Visual projection, compile, passive PDF sync and wrap toggles', async ({ page }) => {
  test.setTimeout(600_000);
  const workspaceName = `cert-caret-r14-20-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  const source = page.getByLabel('LaTeX source editor');
  const longParagraph = 'Long wrapping certification content '.repeat(80);
  const document = [
    '\\documentclass{article}',
    '\\usepackage{amsmath}',
    '\\begin{document}',
    ...Array.from({ length: 500 }, (_, index) => `Certification line ${index + 1}.`),
    longParagraph,
    '\\end{document}',
    '',
  ].join('\n');
  await source.fill(document);
  await source.press(saveShortcut);
  await expect(page.getByText('Saved', { exact: true })).toBeVisible();
  await source.press(process.platform === 'darwin' ? 'Meta+ArrowDown' : 'Control+End');
  await page.keyboard.type(' caret-anchor');
  const anchor = await currentCursorOffset(page);
  expect(anchor).not.toBeNull();
  const anchorValue = anchor ?? '';
  await expect(page.getByTestId('latex-cursor-location')).toContainText(/Ln 50\d/);

  // Autosave must persist without changing selection.
  await expect(page.getByText('Saved', { exact: true })).toBeVisible({ timeout: 20_000 });
  await expectCursorOffset(page, anchorValue);

  // Project semantic refresh is passive.
  await page.getByRole('button', { name: 'Project Outline' }).click();
  await page.getByRole('button', { name: 'Refresh project outline' }).click();
  await page.waitForTimeout(300);
  await expectCursorOffset(page, anchorValue);

  // File-tree refresh is also passive and runs through the real workspace command.
  await pressAppShortcut(page, 'k');
  const omni = page.getByRole('dialog', { name: 'omni command bar' });
  await omni.getByPlaceholder('Type a command or search...').fill('Refresh Files');
  await omni.getByText('Refresh Files', { exact: true }).click();
  await expectCursorOffset(page, anchorValue);

  // Building a Visual projection and returning to Source restores the same caret.
  await page.getByRole('button', { name: 'Visual' }).click();
  await expect(page.locator('.interactive-latex-visual-editor')).toBeVisible();
  await page.getByRole('button', { name: 'Source' }).click();
  await expectCursorOffset(page, anchorValue);

  // Soft wrap is a Compartment reconfiguration: same EditorView and same caret.
  const cmEditor = page.locator('[data-latex-codemirror] .cm-editor');
  await cmEditor.evaluate((element) => {
    (element as HTMLElement).dataset.r1420Identity = 'same-view';
  });
  const wrap = page.getByTestId('latex-soft-wrap-toggle');
  const wasWrapped = (await wrap.getAttribute('aria-pressed')) === 'true';
  if (!wasWrapped) await wrap.click();
  await expect(page.locator('[data-latex-codemirror] .cm-line').last()).toHaveCSS('white-space', 'pre-wrap');
  await expectCursorOffset(page, anchorValue);
  await wrap.click();
  await expect(page.locator('[data-latex-codemirror] .cm-line').last()).toHaveCSS('white-space', 'pre');
  await expect(cmEditor).toHaveAttribute('data-r1420-identity', 'same-view');
  await expectCursorOffset(page, anchorValue);
  await wrap.click();
  await expect(cmEditor).toHaveAttribute('data-r1420-identity', 'same-view');
  await expectCursorOffset(page, anchorValue);

  // Compilation and passive PDF -> Source scroll sync may reveal but never select.
  await compileCurrentProject(page);
  await expectCursorOffset(page, anchorValue);
  const editorFollows = page.getByLabel('Editor follows PDF scroll');
  if (!(await editorFollows.isChecked())) await editorFollows.check();
  const pdfScroll = page.getByTestId('interactive-pdf-scroll-container');
  await pdfScroll.evaluate((element) => {
    element.scrollTop = element.scrollHeight;
    element.dispatchEvent(new Event('scroll'));
  });
  await page.waitForTimeout(700);
  await expectCursorOffset(page, anchorValue);
});

test('R14.20 certifies the core Source toolbar command matrix', async ({ page }) => {
  test.setTimeout(600_000);
  await page.setViewportSize({ width: 1920, height: 1080 });
  const workspaceName = `cert-source-commands-r14-20-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  await createTextFile(page, 'refs.bib', [
    '@article{r1420,',
    '  author = {Learnitive},',
    '  title = {R14.20 Certification Reference},',
    '  year = {2026}',
    '}',
    '',
  ].join('\n'));
  await page.getByRole('link', { name: /^main\.tex$/ }).click();
  const source = page.getByLabel('LaTeX source editor');

  const wrappingCommands = [
    ['Bold', '\\textbf{TARGET}'],
    ['Italic', '\\textit{TARGET}'],
    ['Underline', '\\underline{TARGET}'],
    ['Reference', '\\ref{TARGET}'],
    ['Label', '\\label{TARGET}'],
    ['Footnote', '\\footnote{TARGET}'],
    ['Hyperlink', '\\href{}{TARGET}'],
  ] as const;
  for (const [label, expected] of wrappingCommands) {
    await source.fill('TARGET');
    await source.press(selectAllShortcut);
    await clickSourceToolbar(page, label);
    await expect(source).toContainText(expected);
  }

  const insertionCommands = [
    ['Section', '\\section{'],
    ['Subsection', '\\subsection{'],
    ['Itemize', '\\begin{itemize}'],
    ['Enumerate', '\\begin{enumerate}'],
    ['Figure', '\\begin{figure}'],
    ['Table', '\\begin{table}'],
  ] as const;
  for (const [label, expected] of insertionCommands) {
    await source.fill('');
    await clickSourceToolbar(page, label);
    await expect(source).toContainText(expected);
  }

  for (const [mode, expected] of [
    ['Inline', '\\(r_{14.20}\\)'],
    ['Display', '\\[\nr_{14.20}\n\\]'],
    ['Equation', '\\begin{equation}\nr_{14.20}\n\\end{equation}'],
  ] as const) {
    await source.fill('');
    await clickSourceToolbar(page, 'Equation editor');
    const math = page.getByTestId('mathlive-equation-editor');
    await math.getByRole('button', { name: mode, exact: true }).click();
    await setMathValue(math, 'r_{14.20}');
    await math.getByRole('button', { name: 'Apply' }).click();
    await expect(source).toContainText(expected);
  }

  await source.fill('');
  await clickSourceToolbar(page, 'Citation');
  const citation = page.getByRole('dialog').filter({ hasText: 'Insert citation' });
  await citation.getByText(/R14\.20 Certification Reference/).click();
  await citation.getByRole('button', { name: 'Use 1 citation' }).click();
  await expect(source).toContainText('\\cite{r1420}');
});

test('R14.20 certifies the core Visual toolbar/slash command matrix including Label', async ({ page }) => {
  test.setTimeout(900_000);
  const workspaceName = `cert-visual-commands-r14-20-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });
  await createTextFile(page, 'refs.bib', [
    '@article{r1420,',
    '  author = {Learnitive},',
    '  title = {R14.20 Visual Reference},',
    '  year = {2026}',
    '}',
    '',
  ].join('\n'));
  await page.getByRole('link', { name: /^main\.tex$/ }).click();
  const source = page.getByLabel('LaTeX source editor');

  const base = [
    '\\documentclass{article}',
    '\\usepackage{hyperref}',
    '\\usepackage{graphicx}',
    '\\begin{document}',
    '\\section{Base}\\label{sec:base}',
    '',
    'Visual command target.',
    '',
    '\\end{document}',
    '',
  ].join('\n');

  async function resetVisual() {
    await page.getByRole('button', { name: 'Source' }).click();
    await source.fill(base);
    await source.press(saveShortcut);
    await page.getByRole('button', { name: 'Visual' }).click();
    await expect(page.locator('.interactive-latex-visual-editor')).toBeVisible();
  }

  // Selection toolbar formatting + Link.
  for (const [label, expected] of [
    ['Bold', '\\textbf{command}'],
    ['Italic', '\\emph{command}'],
    ['Underline', '\\underline{command}'],
  ] as const) {
    await resetVisual();
    await selectVisualText(page, '.interactive-visual-paragraph', 'command');
    const toolbar = page.getByRole('toolbar', { name: 'Text formatting' });
    await expect(toolbar).toBeVisible();
    await toolbar.getByRole('button', { name: label }).click();
    await page.getByRole('button', { name: 'Source' }).click();
    await expect(source).toContainText(expected);
  }

  await resetVisual();
  await selectVisualText(page, '.interactive-visual-paragraph', 'command');
  let toolbar = page.getByRole('toolbar', { name: 'Text formatting' });
  await toolbar.getByRole('button', { name: 'Link' }).click();
  const linkEditor = page.getByTestId('latex-visual-link-editor');
  await linkEditor.getByLabel('Link URL').fill('https://example.com/r14-20');
  await linkEditor.getByRole('button', { name: 'Apply' }).click();
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\href{https://example.com/r14-20}{command}');

  const slashCommands = [
    ['sec', 'Section', '\\section{Section title}'],
    ['subsection', 'Subsection', '\\subsection{Subsection title}'],
    ['item', 'Itemize', '\\begin{itemize}'],
    ['enumer', 'Enumerate', '\\begin{enumerate}'],
    ['table', 'Table', '\\begin{table}'],
    ['label', 'Label', '\\label{label}'],
    ['foot', 'Footnote', '\\footnote{Footnote text}'],
  ] as const;
  for (const [query, label, expected] of slashCommands) {
    await resetVisual();
    const slash = await openVisualSlash(page, 'Visual command target.', query);
    await slash.getByText(label, { exact: true }).click();
    await page.getByRole('button', { name: 'Source' }).click();
    await expect(source).toContainText(expected);
  }

  for (const [query, label, expected] of [
    ['inline', 'Inline Math', '\\(r_{14.20}\\)'],
    ['equation', 'Equation', '\\begin{equation}\nr_{14.20}\n\\end{equation}'],
  ] as const) {
    await resetVisual();
    const slash = await openVisualSlash(page, 'Visual command target.', query);
    await slash.getByText(label, { exact: true }).click();
    const math = page.getByTestId('mathlive-equation-editor');
    await setMathValue(math, 'r_{14.20}');
    await math.getByRole('button', { name: 'Apply' }).click();
    await page.getByRole('button', { name: 'Source' }).click();
    await expect(source).toContainText(expected);
  }

  await resetVisual();
  let slash = await openVisualSlash(page, 'Visual command target.', 'cit');
  await slash.getByText('Citation', { exact: true }).click();
  const citation = page.getByRole('dialog').filter({ hasText: 'Insert citation' });
  await citation.getByText(/R14\.20 Visual Reference/).click();
  await citation.getByRole('button', { name: 'Use 1 citation' }).click();
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\cite{r1420}');

  await resetVisual();
  slash = await openVisualSlash(page, 'Visual command target.', 'ref');
  await slash.getByText('Reference', { exact: true }).click();
  const reference = page.getByRole('dialog', { name: 'Insert reference' });
  await reference.getByText('sec:base', { exact: true }).click();
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\ref{sec:base}');

  await resetVisual();
  slash = await openVisualSlash(page, 'Visual command target.', 'fig');
  await slash.getByText('Figure', { exact: true }).click();
  await page.getByLabel('Insert Visual figure').setInputFiles({
    name: 'r14-20.png',
    mimeType: 'image/png',
    buffer: PNG_1X1,
  });
  await expect(page.locator('.interactive-latex-visual-editor').locator('img').first()).toBeVisible();
  await page.getByRole('button', { name: 'Source' }).click();
  await expect(source).toContainText('\\begin{figure}');
  await expect(source).toContainText('r14-20.png');
});
