// Shared Playwright helpers for the editor tests.
//
// What's here
// ───────────
// • `gotoFreshEditor(page)` — navigate to the editor with a clean
//   localStorage and a freshly-unregistered service worker, so each
//   test starts from a known empty canvas regardless of state left
//   behind by previous tests or local browsing.
//
// • `addTextViaSidebar(page, { text })` — drive the Text tab to add
//   a text element. Used by multiple specs so the pattern lives in
//   one place.
//
// • `getElementsCount(page)` — read the LayersPanel's "Layers (N)"
//   title to assert how many elements exist on the canvas. The
//   panel is the canonical user-visible source of truth for that
//   count.
//
// • `pressUndo(page)` / `pressRedo(page)` — Cmd+Z / Cmd+Shift+Z
//   wrappers that send the same keystrokes the user would. Modifier
//   is meta on macOS, control elsewhere; we detect via the
//   userAgent. (The app's keyboard handler accepts either modifier
//   so we could hardcode one, but using the platform-correct one
//   keeps the tests honest about what real users do.)
//
// Why these live here rather than inline in each spec
// ───────────────────────────────────────────────────
// Three reasons. (1) Several specs need fresh-canvas setup; if it
// drifts inline-in-each-test, "fresh" stops meaning the same thing
// across the suite. (2) The localStorage + service-worker reset is
// non-obvious and easy to skip; centralising it ensures every test
// gets the full reset rather than just the obvious bits. (3) When
// the app's selectors change (e.g. someone renames the LayersPanel
// title), one edit here updates every test rather than touching each
// spec file.

import { expect } from '@playwright/test';

/**
 * Navigate to the editor with a fully fresh state.
 *
 * The order matters:
 *   1. goto('/') first so we have a page context to evaluate against.
 *      Without this, `evaluate` has nowhere to run.
 *   2. Clear localStorage AND unregister any service workers AND
 *      clear caches. The PWA's service worker caches static assets;
 *      stale caches between test runs can mask real regressions
 *      (e.g. an app code change that breaks the editor but a cached
 *      SW returns the old version).
 *   3. Reload to apply the wipe — the first goto loaded the app
 *      against whatever state was there, so step 2's wipe wouldn't
 *      affect the current page without a reload.
 *   4. Wait for the canvas to mount before returning. The Konva
 *      Stage takes a few frames after the first paint to be ready
 *      for interaction; clicking buttons before then can race.
 *
 * The wait targets are:
 *   • `.canvas-area` — the DOM container is present immediately
 *     once React mounts.
 *   • A document-fonts-ready check — without this, text-related
 *     assertions can race against the woff2 fetches that
 *     useFontsReady waits for. measureTextWidth returns
 *     fallback-font values until the real fonts arrive.
 */
export async function gotoFreshEditor(page) {
  // Step 1: land on the editor so we have storage access.
  await page.goto('/');

  // Step 2: wipe persisted state + SW caches.
  await page.evaluate(async () => {
    try { localStorage.clear(); } catch { /* ignore */ }
    try { sessionStorage.clear(); } catch { /* ignore */ }
    // Unregister all service workers. The app's PWA registers one
    // on first load; without unregistering, a SW from a previous
    // test run can serve stale assets.
    if ('serviceWorker' in navigator) {
      const regs = await navigator.serviceWorker.getRegistrations();
      await Promise.all(regs.map((r) => r.unregister()));
    }
    // Wipe Cache API entries (the SW's storage layer).
    if ('caches' in window) {
      const keys = await caches.keys();
      await Promise.all(keys.map((k) => caches.delete(k)));
    }
  });

  // Step 3: reload to apply the wipe.
  await page.reload();

  // Step 4: wait for the editor's primary surfaces.
  // canvas-area exists once App.jsx renders. The stage's <canvas>
  // tag is inside but doesn't need to be specifically awaited —
  // it's children of canvas-area.
  await page.locator('.canvas-area').waitFor({ state: 'visible' });

  // Wait for fonts so the text-width measurements settle. This
  // mirrors what useFontsReady does inside the app — both run
  // until document.fonts.ready resolves. Without it,
  // placeTextCentered uses fallback widths and the placement-
  // assertion in tests can be off by tens of pixels.
  await page.evaluate(() => document.fonts.ready);
}

/**
 * Add a text element via the Text tab. Returns once the new element
 * has appeared in the LayersPanel — at which point it's safe to
 * make assertions about its presence on the canvas.
 *
 * The flow is:
 *   1. Click the Text tab in the sidebar to make sure it's active.
 *      In a fresh editor the default tab is Upload, so we have to
 *      switch.
 *   2. Type into the "Your message" textarea, replacing the
 *      pre-populated "Your text here" draft.
 *   3. Click "Add text to canvas".
 *   4. Wait for the Layers panel to show the new layer.
 *
 * We don't change fontFamily / fontSize / fill here — they default
 * to whatever the draft has (DEFAULT_DRAFT in TextTab.jsx). Tests
 * that care about a specific font / size can call the relevant
 * controls afterward.
 */
export async function addTextViaSidebar(page, { text }) {
  // Ordering matters: deselect FIRST, switch to Text tab SECOND.
  // ────────────────────────────────────────────────────────────
  // On desktop, App.jsx derives `editingTextElement` directly from
  // `selectedElement?.type === 'text'`, AND it has an auto-tab-switch
  // effect that runs on every text-selection transition:
  //
  //   text → not-text : auto-switches sidebar from 'text' → 'upload'
  //   anything → text : auto-switches sidebar to 'text'
  //
  // So after a previous addTextViaSidebar:
  //   • A text element is selected.
  //   • The sidebar is on the 'text' tab.
  //   • TextTab is rendered in EDIT MODE (the Add button is gone,
  //     replaced by fields editing the selected element).
  //
  // The naive ordering (click Text tab → deselect → wait for Add
  // button) doesn't work, because the deselect ALSO fires the
  // auto-tab-switch which snaps the sidebar back to 'upload'. The
  // Text tab unmounts entirely; the Add button isn't hidden, it's
  // gone with the rest of the tab. The waitFor times out looking
  // for a button that doesn't exist anywhere on the page.
  //
  // Correct ordering: deselect FIRST (sidebar auto-snaps to 'upload',
  // any prior selection clears), THEN click the Text tab to switch
  // back into it. Our explicit click is the LAST tab interaction,
  // so it wins; TextTab mounts in draft mode (no selected text
  // element to bind to) and renders the Add button.
  //
  // Deselection strategy
  // ────────────────────
  // We tried three approaches; only the third works, and the journey
  // is documented so a future maintainer doesn't repeat it.
  //
  // 1. Press Escape. App.jsx wires Escape to `deselectAll`, but the
  //    keyboard handler has an input-gate early return on INPUT /
  //    TEXTAREA targets. After Playwright's previous `.fill()` the
  //    textarea owns focus, so bubbled keydown's `e.target` is the
  //    textarea and the handler bails. Programmatic blur didn't
  //    help — Playwright tracks focus internally and routes
  //    keystrokes through that internal state, not document.activeElement.
  //
  // 2. Click `.canvas-area` at `{x:5, y:5}`. App.jsx's mousedown
  //    deselect handler skips clicks inside the Konva stage container
  //    (stageContainerRef). The stage container fills most of
  //    canvas-area, so corner positions land inside it and the
  //    handler bails. The pink-frame deselect zone is narrower than
  //    it looks from the docblock alone.
  //
  // 3. (Current) Dispatch a real `mousedown` event with the
  //    canvas-area element itself as `e.target`. The handler's
  //    early-return checks pass cleanly: canvasRef.contains(canvas-area)
  //    is true (a node contains itself), and none of the excluded
  //    refs (toolbar / zoom / stage container) CONTAIN their
  //    ancestor canvas-area. The handler proceeds to deselectAll,
  //    which clears selectedId AND triggers the auto-tab-switch to
  //    'upload'.
  const addButton = page.getByRole('button', { name: 'Add text to canvas' });

  // Cheap selection probe: if there's no LayersPanel content (count is
  // zero) OR the Text tab isn't even active, there's no selection to
  // clear and we can skip the deselect path entirely. The probe avoids
  // a 300ms isVisible wait on every call when we'd just no-op.
  const currentCount = await getElementsCount(page);
  if (currentCount > 0) {
    // Deselect via a dispatched mousedown on canvas-area itself. This
    // is the only reliable path; see strategy notes above.
    await page.evaluate(() => {
      const area = document.querySelector('.canvas-area');
      if (!area) return;
      const event = new MouseEvent('mousedown', {
        bubbles: true,
        cancelable: true,
        view: window,
      });
      area.dispatchEvent(event);
    });
  }

  // Now switch to the Text tab. After the deselect above, sidebar
  // is on 'upload' (auto-snapped); our explicit click moves it to
  // 'text' for the add. TextTab mounts in draft mode and renders
  // the Add button.
  await page.getByRole('tab', { name: /text/i }).click();

  // Wait for the Add button. With the correct tab+deselect order,
  // this should be near-instant.
  await addButton.waitFor({ state: 'visible', timeout: 5000 });

  // Fill the textarea. The label is "Your message" per TextTab.jsx
  // (`<label htmlFor="tt-text-input">Your message</label>`).
  const textarea = page.getByLabel('Your message');
  await textarea.fill(text);

  // Capture the current layer count so we can wait for it to
  // increment. Reading the title text avoids us having to count
  // rendered <li> rows, which can be slower to read than the
  // pre-aggregated count in the title.
  const before = await getElementsCount(page);

  // Step 3: submit. The button is exposed by its visible text.
  await addButton.click();

  // Step 4: wait for the count to increment. expect.poll handles
  // the race between the click and the React commit + Konva mount.
  await expect.poll(() => getElementsCount(page)).toBe(before + 1);
}

/**
 * Read the current element count from the LayersPanel title.
 * Returns 0 when no layers exist (the panel renders an empty-state
 * banner instead of the "Layers (N)" title in that case).
 *
 * The panel's title is `Layers (N)` per LayersPanel.jsx:
 *   <h3 className="layers-title">Layers ({elements.length})</h3>
 *
 * We parse the N out of the title rather than counting <li>
 * elements because the panel's empty state doesn't render any
 * <li>s OR the title — we'd need two different code paths for
 * "zero" vs "non-zero" if we counted rows.
 */
export async function getElementsCount(page) {
  // Title may not exist if the panel is showing its empty state.
  // Use .count() rather than waitFor to make this non-blocking.
  const title = page.locator('.layers-title');
  if ((await title.count()) === 0) return 0;
  const text = await title.first().textContent();
  // Title format: "Layers (N)". Extract N. Defensive fallback to 0
  // if the format changes (test still fails downstream with a
  // useful message rather than throwing here).
  const match = /\((\d+)\)/.exec(text || '');
  return match ? parseInt(match[1], 10) : 0;
}

/**
 * Send a Cmd+Z (macOS) / Ctrl+Z (other) keystroke. The app's
 * keyboard handler in App.jsx accepts either modifier, but using
 * the platform-correct one keeps the test mirror of real user
 * behaviour.
 *
 * The keystroke is sent to `document.body` (Playwright's default
 * for page.keyboard.press) rather than a specific element. The
 * app's handler is attached to `window`, so the focus target
 * doesn't matter — as long as it isn't inside an <input> or
 * <textarea>, where the handler's early-return skips the
 * shortcut.
 */
export async function pressUndo(page) {
  const isMac = process.platform === 'darwin';
  await page.keyboard.press(isMac ? 'Meta+z' : 'Control+z');
}

export async function pressRedo(page) {
  // The handler accepts either Cmd+Shift+Z or Cmd+Y; we use
  // Shift+Z because it's the more conventional shortcut and
  // covers the same code path.
  const isMac = process.platform === 'darwin';
  await page.keyboard.press(isMac ? 'Meta+Shift+z' : 'Control+Shift+z');
}
