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

/**
 * Regression cover for READINGFUL-0V-G — "AbortError: The play() request was
 * interrupted by a call to pause()".
 *
 * LOCAL ONLY. No CI workflow runs Playwright, and this spec hardcodes the
 * readingful.test host and seeded credentials, so it will not pass anywhere
 * else. Run it by hand.
 *
 * The production trigger is a slow connection: `play()` stays pending while the
 * mp4 buffers and the student's next tap calls `pause()` inside that window,
 * rejecting the promise. Locally the story has media rows but no audio files on
 * disk, so that buffering window cannot occur naturally; this installs the
 * documented browser contract (https://goo.gl/LdLk22) and runs the real bundle
 * against it. The deterministic cover is tests/js/hooks/useStoryAudio.abort.test.tsx.
 */

// The app answers on its own host, and /story/{slug} bounces guests to the
// homepage, so this spec cannot use the config's localhost:8000 baseURL.
const BASE_URL = 'http://readingful.test';
const STORY_PATH = '/story/a-stubborn-kitty';

/** The play button is the only element carrying this custom property. */
const PLAY_BUTTON = 'button[style*="--story-play-bg"]';

test.use({ baseURL: BASE_URL });

async function installMediaContract(page: Page) {
  await page.addInitScript(() => {
    const w = window as unknown as { __rejections: string[] };
    w.__rejections = [];
    window.addEventListener('unhandledrejection', (event) => {
      w.__rejections.push(String(event.reason));
    });

    let pending: Array<(reason: DOMException) => void> = [];

    HTMLMediaElement.prototype.play = function () {
      return new Promise<void>((_resolve, reject) => {
        pending.push(reject);
      });
    };

    HTMLMediaElement.prototype.pause = function () {
      const rejectors = pending;
      pending = [];
      rejectors.forEach((reject) =>
        reject(
          new DOMException(
            'The play() request was interrupted by a call to pause(). https://goo.gl/LdLk22',
            'AbortError'
          )
        )
      );
    };
  });
}

/**
 * Logs in through the JSON endpoint. The UI login is a multi-step modal, and a
 * teacher on a trial subscription is bounced off the reader by StoryController,
 * so this signs in as an admin, who skips that gate.
 */
async function loginAsAdmin(page: Page, context: BrowserContext) {
  await page.goto('/');
  await page.waitForLoadState('domcontentloaded');
  const xsrf = (await context.cookies()).find((cookie) => cookie.name === 'XSRF-TOKEN');

  const response = await page.request.post('/api/login', {
    headers: {
      'X-XSRF-TOKEN': decodeURIComponent(xsrf?.value ?? ''),
      Accept: 'application/json',
    },
    form: { username: 'admin@example.com', password: 'password' },
    maxRedirects: 0,
  });

  expect(response.status()).toBe(302);
}

async function openReader(page: Page) {
  await page.goto(STORY_PATH);

  // The reader is a lazily imported Inertia page. A dev server that has not
  // re-optimised its deps serves a 504 for it and nothing mounts — skip rather
  // than report a failure that says nothing about the fix.
  const mounted = await page
    .waitForSelector(PLAY_BUTTON, { timeout: 20000 })
    .then(() => true)
    .catch(() => false);

  test.skip(
    !mounted,
    'Story reader did not mount — Vite dev server is not serving Pages/Story.tsx'
  );
}

async function rejections(page: Page): Promise<string[]> {
  await page.waitForTimeout(300); // let the browser dispatch unhandledrejection
  return page.evaluate(() => (window as unknown as { __rejections: string[] }).__rejections);
}

test.describe('interrupted audio playback on the story reader', () => {
  test.beforeEach(async ({ page, context }) => {
    await installMediaContract(page);
    await loginAsAdmin(page, context);
    await openReader(page);
  });

  test('pausing before playback begins raises no unhandled rejection', async ({ page }) => {
    await page.click(PLAY_BUTTON);
    await page.click(PLAY_BUTTON); // pause while play() is still pending

    expect(await rejections(page)).toEqual([]);
  });

  test('clicking a sentence mid-clip raises no unhandled rejection', async ({ page }) => {
    await page.click(PLAY_BUTTON);
    await page
      .locator('span')
      .filter({ hasText: /\w{10,}/ })
      .first()
      .click();

    expect(await rejections(page)).toEqual([]);
  });
});
