// E2E: undo/redo flow.
//
// What this guards
// ────────────────
// The history subsystem is the most bug-bitten area of the codebase
// (see the May 22 / May 23 Refinements notes). useDesignEditor.test.js
// covers the hook-level behaviour exhaustively, but the keyboard
// shortcuts (Cmd+Z / Cmd+Shift+Z) are attached to the window via a
// global useEffect in App.jsx — that wiring isn't exercised by Vitest.
// This spec drives the actual keystrokes and verifies the right
// things happen.
//
// Two scenarios in particular are worth catching at this layer:
//
//   1. The keyboard handler's input/textarea exclusion. The handler
//      bails out early when the focused element is an <input> or
//      <textarea>, so the user can type a `z` into the text field
//      without accidentally undoing. Vitest can't test this because
//      it doesn't render real form elements with focus state.
//
//   2. The HistoryControls buttons disabling/enabling correctly. The
//      pills have `disabled` attributes wired to canUndo/canRedo,
//      and the visual state of those buttons is what tells the
//      user "you can / can't go back further." A bug where the
//      buttons stay enabled past the history floor would only be
//      visible at the DOM-attribute level.

import { test, expect } from '@playwright/test';
import {
  gotoFreshEditor,
  addTextViaSidebar,
  getElementsCount,
  pressUndo,
  pressRedo,
} from './fixtures/editor.js';

test.beforeEach(async ({ page }) => {
  await gotoFreshEditor(page);
});

test('Cmd/Ctrl+Z removes the most recently added element', async ({ page }) => {
  // The headline undo test. Add an element, undo, verify it's gone.
  // This single keystroke covers the whole pipeline: the keyboard
  // handler in App.jsx fires undo(), useDesignEditor walks the
  // history pointer back, setElements applies the previous snapshot,
  // LayersPanel re-renders.
  await addTextViaSidebar(page, { text: 'Will be undone' });
  expect(await getElementsCount(page)).toBe(1);

  await pressUndo(page);
  // The deleted element is gone; count drops to 0.
  expect(await getElementsCount(page)).toBe(0);
});

test('Cmd/Ctrl+Shift+Z restores an undone element', async ({ page }) => {
  // Redo after undo. The post-redo state must match the original
  // (same text on the same element) — a regression where redo
  // restored a different snapshot than what was undone would fail
  // the layer-name assertion.
  await addTextViaSidebar(page, { text: 'Resurrected' });
  await pressUndo(page);
  expect(await getElementsCount(page)).toBe(0);

  await pressRedo(page);
  expect(await getElementsCount(page)).toBe(1);
  await expect(page.locator('.layers-item-name', { hasText: 'Resurrected' })).toBeVisible();
});

test('typing `z` in the text field does NOT undo (keyboard handler input gate)', async ({ page }) => {
  // The keyboard handler in App.jsx has an explicit early-return:
  //   if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
  // Without this, typing a `z` into the textarea while holding a
  // modifier would hijack the keystroke. (Just typing `z` without
  // a modifier doesn't trigger the undo branch anyway, but Cmd+Z
  // is the danger — except even Cmd+Z is suppressed because the
  // user's cursor is in a field where Cmd+Z is the OS-native
  // "undo last typed character" shortcut, which the browser
  // handles.)
  //
  // We test the simpler case here: pressing Cmd+Z while focused
  // on the textarea should NOT remove the element. The OS-native
  // textarea undo may or may not fire — we don't care; we care
  // that OUR keyboard handler doesn't fire and remove an element
  // from the canvas.
  await addTextViaSidebar(page, { text: 'Safe' });
  expect(await getElementsCount(page)).toBe(1);

  // Focus the textarea by clicking it explicitly. addTextViaSidebar
  // leaves focus elsewhere by the time it returns; we have to
  // re-focus deterministically.
  const textarea = page.getByLabel('Your message');
  await textarea.click();
  await expect(textarea).toBeFocused();

  // Now press Cmd+Z. The app's handler should bail out at the
  // tagName check. The element on the canvas stays.
  await pressUndo(page);

  // Verification: element count is still 1.
  expect(await getElementsCount(page)).toBe(1);
});

test('Undo and Redo buttons in the bottom bar reflect history state', async ({ page }) => {
  // HistoryControls renders two buttons with aria-label "Undo" /
  // "Redo" and a `disabled` attribute wired to canUndo / canRedo.
  // The button states are how users (and screen readers) see
  // whether stepping further is available.
  //
  // History initialization model (confirmed by running the suite):
  // a fresh editor's history is seeded with the empty snapshot at
  // idx=0. canUndo = `idx > 0`, so a fresh editor has canUndo false.
  // The FIRST add pushes a new snapshot and advances idx to 1 — at
  // which point canUndo flips to true because the user can step
  // back to the empty floor. This contradicted my initial mental
  // model ("first add gives idx=0") but matches what the app
  // actually does; the May 22 cleanup arc relies on that empty
  // floor being there as a restoration target.
  const undoButton = page.getByRole('button', { name: 'Undo' });
  const redoButton = page.getByRole('button', { name: 'Redo' });

  // Fresh editor: history=[empty], idx=0.
  //   canUndo = 0 > 0          = false  (we're at the floor)
  //   canRedo = 0 < length-1=0 = false  (nothing to redo)
  await expect(undoButton).toBeDisabled();
  await expect(redoButton).toBeDisabled();

  // After one add: history=[empty, [A]], idx=1.
  //   canUndo = 1 > 0          = true   (can step back to empty)
  //   canRedo = 1 < length-1=1 = false  (we're at the head)
  await addTextViaSidebar(page, { text: 'First' });
  await expect(undoButton).toBeEnabled();
  await expect(redoButton).toBeDisabled();

  // After two adds: history=[empty, [A], [A,B]], idx=2.
  //   canUndo = 2 > 0          = true
  //   canRedo = 2 < length-1=2 = false
  await addTextViaSidebar(page, { text: 'Second' });
  await expect(undoButton).toBeEnabled();
  await expect(redoButton).toBeDisabled();

  // Click Undo: idx 2 → 1. Now we're mid-history with both
  // directions available.
  //   canUndo = 1 > 0          = true
  //   canRedo = 1 < length-1=2 = true   (can re-apply the second add)
  await undoButton.click();
  await expect(undoButton).toBeEnabled();
  await expect(redoButton).toBeEnabled();

  // Click Redo: idx 1 → 2. Back to post-two-adds state.
  await redoButton.click();
  await expect(undoButton).toBeEnabled();
  await expect(redoButton).toBeDisabled();

  // Finally, undo twice to reach the floor and confirm undo
  // disables at idx=0. This is the assertion the first-add test
  // ABOVE used to (incorrectly) make against a one-add history;
  // the only way to actually get there is to wind all the way
  // back to the seeded empty floor.
  await undoButton.click();   // idx 2 → 1
  await undoButton.click();   // idx 1 → 0  (the floor)
  await expect(undoButton).toBeDisabled();
  await expect(redoButton).toBeEnabled();
});

test('a new action after undo discards the redo stack', async ({ page }) => {
  // Standard editor behaviour: after undo, redo is available. But
  // a new edit branches history forward from the current idx,
  // discarding anything that was previously to the right of it.
  // This is the redo-stack truncation tested in
  // useDesignEditor.test.js at the hook level; here we confirm
  // it works end-to-end through the real keyboard handler.
  await addTextViaSidebar(page, { text: 'A' });
  await addTextViaSidebar(page, { text: 'B' });
  await pressUndo(page);
  // Now redo would restore "B".
  await expect(page.getByRole('button', { name: 'Redo' })).toBeEnabled();

  // New branch: add a different element. This should truncate the
  // redo stack — "B" is gone from history.
  await addTextViaSidebar(page, { text: 'C' });
  await expect(page.getByRole('button', { name: 'Redo' })).toBeDisabled();

  // Undoing now should land on the "just A" state, NOT "A + B".
  await pressUndo(page);
  expect(await getElementsCount(page)).toBe(1);
  await expect(page.locator('.layers-item-name', { hasText: 'A' })).toBeVisible();
  // "B" should not be findable — it was truncated when we added C.
  await expect(page.locator('.layers-item-name', { hasText: 'B' })).toHaveCount(0);
});
