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

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

function multiPageDocument(): string {
  const parts = ['\\documentclass{article}', '\\begin{document}'];
  for (let page = 1; page <= 10; page += 1) {
    parts.push(`\\section*{Page ${page}}`);
    for (let line = 1; line <= 12; line += 1) {
      parts.push(`Page ${page} line ${line}. This text provides a stable SyncTeX mapping.`);
    }
    if (page < 10) parts.push('\\newpage');
  }
  parts.push('\\end{document}', '');
  return parts.join('\n');
}

test('R14.15 keeps split-workspace toolbars fixed and synchronizes editor/PDF scrolling', async ({ page }) => {
  const workspaceName = `split-r14-15-${Date.now()}`;
  await createBrowserWorkspace(page, { workspaceName });

  const source = page.getByLabel('LaTeX source editor');
  await source.fill(multiPageDocument());
  await source.press(saveShortcut);

  await page.getByRole('button', { name: 'Compile', exact: true }).click();
  await expect(page.getByTestId('interactive-pdf-preview').locator('canvas')).toBeVisible({ timeout: 180_000 });

  const split = page.getByTestId('interactive-split-workspace');
  const editorToolbar = page.getByTestId('interactive-editor-toolbar');
  const pdfToolbar = page.getByTestId('interactive-pdf-toolbar');
  const pdfScroll = page.getByTestId('pdf-continuous-scroll');
  const sourceScroll = page.locator('[data-latex-codemirror] .cm-scroller');
  const sourceHost = page.locator('[data-latex-codemirror]');

  await expect(split).toBeVisible();
  await expect(pdfScroll).toBeVisible();
  await expect(page.locator('[data-pdf-page-wrapper]')).toHaveCount(10);
  await expect(page.getByText(/SyncTeX ready/)).toBeVisible({ timeout: 30_000 });

  const editorToolbarTop = (await editorToolbar.boundingBox())?.y;
  const pdfToolbarTop = (await pdfToolbar.boundingBox())?.y;
  expect(editorToolbarTop).toBeDefined();
  expect(pdfToolbarTop).toBeDefined();

  // PDF -> Source: isolate the reverse direction. Use a real wheel gesture
  // so the PDF surface explicitly owns the interaction rather than relying on
  // a synthetic scroll event that must be treated as programmatic.
  const pdfFollows = page.getByLabel('PDF follows editor scroll');
  const editorFollows = page.getByLabel('Editor follows PDF scroll');
  if (await pdfFollows.isChecked()) await pdfFollows.uncheck();
  if (!(await editorFollows.isChecked())) await editorFollows.check();
  await source.click();
  await source.press(process.platform === 'darwin' ? 'Meta+Home' : 'Control+Home');
  for (let line = 0; line < 24; line += 1) await source.press('ArrowDown');
  const caretBeforePassivePdfScroll = await sourceHost.getAttribute('data-cursor-offset');
  expect(caretBeforePassivePdfScroll).not.toBeNull();
  await sourceScroll.evaluate((element) => { element.scrollTop = 0; });
  await pdfScroll.evaluate((element) => { element.scrollTop = 0; });
  await pdfScroll.hover();
  await page.mouse.wheel(0, 6_000);
  await expect.poll(async () => sourceScroll.evaluate((element) => element.scrollTop), { timeout: 15_000 }).toBeGreaterThan(40);
  await expect(sourceHost).toHaveAttribute('data-cursor-offset', caretBeforePassivePdfScroll ?? '');

  expect(Math.abs(((await editorToolbar.boundingBox())?.y ?? 0) - (editorToolbarTop ?? 0))).toBeLessThan(2);
  expect(Math.abs(((await pdfToolbar.boundingBox())?.y ?? 0) - (pdfToolbarTop ?? 0))).toBeLessThan(2);
  expect(await page.evaluate(() => window.scrollY)).toBe(0);

  // Source -> PDF: isolate the forward direction. A real Source wheel gesture
  // must move the continuous PDF viewport rather than switching a single-page
  // view or requiring a timer window to avoid feedback.
  if (!(await pdfFollows.isChecked())) await pdfFollows.check();
  if (await editorFollows.isChecked()) await editorFollows.uncheck();
  await pdfScroll.evaluate((element) => { element.scrollTop = 0; });
  await sourceScroll.evaluate((element) => { element.scrollTop = 0; });
  await sourceScroll.hover();
  await page.mouse.wheel(0, 6_000);
  await expect.poll(async () => pdfScroll.evaluate((element) => element.scrollTop), { timeout: 15_000 }).toBeGreaterThan(100);

  // Both directions enabled: reproduce the historical trap at the bottom.
  // Source remains the owner while PDF performs its follower scroll, so a new
  // upward Source wheel gesture must take ownership immediately and cannot be
  // reversed by PDF's programmatic scroll event.
  if (!(await editorFollows.isChecked())) await editorFollows.check();
  await sourceScroll.hover();
  await page.mouse.wheel(0, 20_000);
  const sourceBottom = await sourceScroll.evaluate((element) => element.scrollTop);
  expect(sourceBottom).toBeGreaterThan(100);
  await page.mouse.wheel(0, -2_500);
  await expect.poll(
    async () => sourceScroll.evaluate((element) => element.scrollTop),
    { timeout: 15_000 },
  ).toBeLessThan(sourceBottom - 20);
  const sourceAfterUp = await sourceScroll.evaluate((element) => element.scrollTop);
  await page.waitForTimeout(500);
  const sourceAfterFollowerSettles = await sourceScroll.evaluate((element) => element.scrollTop);
  expect(Math.abs(sourceAfterFollowerSettles - sourceAfterUp)).toBeLessThan(80);
  expect(await page.evaluate(() => window.scrollY)).toBe(0);
});
