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

/**
 * Regression cover for ENG-2211 — fades blink for one frame at the start and
 * end of every FadeTransition / FadePresence in Chromium.
 *
 * framer-motion runs opacity on the compositor through WAAPI. When that
 * animation finishes it cancels it and hands the final value back to the
 * element's inline style. Before motion-dom 12.35 the cancel was synchronous
 * but the style write waited for the next motion frame, so Chromium could
 * paint one frame with the stale inline value: a fade-in vanished (still
 * opacity 0) and a fade-out popped back (still opacity 1).
 *
 * The flash itself depends on frame timing and can't be asserted reliably, but
 * its precondition can. This spec records the inline opacity at the instant
 * each finished opacity animation is cancelled and requires it to already be
 * the final keyframe.
 *
 * LOCAL ONLY: it hardcodes the readingful.test host and needs story 8 to be
 * visible to guests (featured or sampled this month) so the "About This
 * Sample" modal opens.
 */

const BASE_URL = 'http://readingful.test';
const STORY_PATH = '/story/a-stubborn-kitty';

test.use({ baseURL: BASE_URL });

type Handoff = { inline: string; final: string };

test('opacity fades hand off to their final style without a stale frame', async ({ page }) => {
  await page.addInitScript(() => {
    const w = window as unknown as { __handoffs: Handoff[] };
    w.__handoffs = [];
    const animate = Element.prototype.animate;
    Element.prototype.animate = function (keyframes, options) {
      const animation = animate.call(this, keyframes, options);
      const opacity = (keyframes as PropertyIndexedKeyframes | null)?.opacity;
      if (!Array.isArray(opacity)) return animation;
      const element = this as HTMLElement;
      const final = String(opacity[opacity.length - 1]);
      const cancel = animation.cancel.bind(animation);
      animation.cancel = () => {
        if (animation.playState === 'finished') {
          w.__handoffs.push({ inline: element.style.opacity, final });
        }
        cancel();
      };
      return animation;
    };
  });

  await page.goto(STORY_PATH);

  const heading = page.getByRole('heading', { name: 'About This Sample' });
  await expect(heading, 'story 8 must be featured or sampled for guests').toBeVisible({
    timeout: 10_000,
  });
  // The backdrop and the card each fade in; let both finish.
  await expect
    .poll(() =>
      page.evaluate(() => (window as unknown as { __handoffs: Handoff[] }).__handoffs.length)
    )
    .toBeGreaterThanOrEqual(2);

  await page.getByText('Close', { exact: true }).click();
  await expect(heading).toBeHidden();

  const handoffs = await page.evaluate(
    () => (window as unknown as { __handoffs: Handoff[] }).__handoffs
  );
  // Fade in (backdrop + card) and fade out (backdrop + card).
  expect(handoffs.filter((h) => h.final === '1').length).toBeGreaterThanOrEqual(2);
  expect(handoffs.filter((h) => h.final === '0').length).toBeGreaterThanOrEqual(2);
  for (const handoff of handoffs) {
    expect(handoff.inline, 'inline opacity when the finished animation was cancelled').toBe(
      handoff.final
    );
  }
});
