import {
  test,
  expect,
  callBinding,
  resetEvents,
  waitForEvent,
  LONG_TRACK,
} from '../support/fixtures.js';

/** The one fixture long enough to still be playing on the next line. */
const longRow = (app: import('@playwright/test').Page) =>
  app.getByTestId('track-row').filter({ hasText: LONG_TRACK }).first();

/**
 * Playback and the queue, driven through the UI and asserted on the
 * events the backend actually emits.
 *
 * Audio really is initialised here: under `dbus-run-session` + Xvfb the
 * PulseAudio socket in /run/user is untouched, so InitSpeaker succeeds
 * and these tracks genuinely play.  A CI container without /run/user
 * needs a null sink; everything except the audio itself still works
 * without one.
 */
test.describe('playback', () => {
  test.beforeEach(async ({ app }) => {
    // The app lands on Home now (H-8), so the track list is a
    // navigation away rather than the first thing on screen.
    await app.getByTestId('nav-tracks').click();
    await callBinding(app, 'queue.Queue.Clear').catch(() => {
      /* older builds may not expose Clear; the specs below do not need it */
    });
    await resetEvents(app);
  });

  test('double-clicking a track plays it', async ({ app }) => {
    await longRow(app).dblclick();

    const changed = await waitForEvent(app, 'TrackChanged');

    expect(changed.data[0]).toBeTruthy();

    // The transport flips to Pause, which is the only place the UI
    // states "we are playing" in a way a user can see.  `exact` is not
    // optional: "Add queue to playlist" also matches /play/i.
    await expect(
      app.getByRole('button', { name: 'Pause', exact: true }),
    ).toBeVisible();

    await expect(app.getByTestId('now-playing-title')).toContainText(
      LONG_TRACK,
    );
  });

  test('the elapsed time advances', async ({ app }) => {
    await longRow(app).dblclick();
    await waitForEvent(app, 'TrackChanged');

    // Not a fixed sleep on a fixed value: assert the observable
    // outcome, which is that the clock is no longer at zero.
    await expect(app.getByTestId('elapsed-time')).not.toHaveText('--:--');
    await expect(app.getByTestId('elapsed-time')).not.toHaveText('00:00', {
      timeout: 15_000,
    });
  });

  test('pause and play round-trip through the backend', async ({ app }) => {
    await longRow(app).dblclick();
    await waitForEvent(app, 'TrackChanged');

    await resetEvents(app);
    await app.getByRole('button', { name: 'Pause', exact: true }).click();
    await waitForEvent(app, 'PlaybackStateChanged');

    await expect(
      app.getByRole('button', { name: 'Play', exact: true }),
    ).toBeVisible();
  });

  test('volume changes are pushed back from Go', async ({ app }) => {
    await resetEvents(app);
    await callBinding(app, 'player.Player.SetVolume', [55]);

    const ev = await waitForEvent(app, 'VolumeChanged');

    expect(ev.data).toEqual([55]);
  });
});

test.describe('queue', () => {
  test.beforeEach(async ({ app }) => {
    // The app lands on Home now (H-8).
    await app.getByTestId('nav-tracks').click();
  });

  test('playing a track queues the list it is in', async ({ app }) => {
    await resetEvents(app);
    await longRow(app).dblclick();
    await waitForEvent(app, 'QueueChanged');

    // The panel has to be open to have rows. A closed one is `width: 0`
    // and now renders no list at all (perf.m7) — before that it kept a
    // virtualizer measuring its window on every queue change, and this
    // assertion passed against a panel nobody could see.
    const queueToggle = app.getByRole('button', { name: 'Toggle queue' });

    await queueToggle.click();

    // Activating one row plays the list that row is in, from that row —
    // the library as displayed, not a queue of one. The row's own
    // position is what `currentIndex` points at.
    const state = await callBinding<{
      tracks: { title: string }[];
      currentIndex: number;
    }>(app, 'queue.Queue.GetState');

    expect(state.tracks.length).toBeGreaterThan(1);
    expect(state.tracks[state.currentIndex]?.title).toContain(LONG_TRACK);

    // The panel shows that queue rather than some other one: every
    // fixture track fits on screen, so the counts are comparable.
    await expect(app.getByTestId('queue-row')).toHaveCount(
      state.tracks.length,
    );

    // Shut it again, and wait until it really is shut. These specs
    // share one backend process in file order, the panel's width is
    // animated, and the transport slides while it closes — a click
    // issued during that lands on whichever button has moved under the
    // pointer, which for the very next test was Repeat rather than
    // Shuffle. Both emit QueueModeChanged, so it failed on the
    // assertion rather than on the wait, one run in two.
    //
    // Waiting on the row count rather than a timeout is also the m7
    // assertion: a closed panel renders no list at all.
    await queueToggle.click();
    await expect(app.getByTestId('queue-row')).toHaveCount(0);
  });

  test('shuffle and repeat toggles report their state', async ({ app }) => {
    const shuffle = app.getByRole('button', { name: 'Shuffle' });

    await resetEvents(app);
    await shuffle.click();
    await waitForEvent(app, 'QueueModeChanged');

    await expect(shuffle).toHaveAttribute('aria-pressed', 'true');

    await shuffle.click();
    await expect(shuffle).toHaveAttribute('aria-pressed', 'false');
  });
});

test.describe('the playing row is findable without colour vision', () => {
  test('the queue marks its current row with a shape and aria-current', async ({
    app,
  }) => {
    await app.getByTestId('nav-tracks').click();
    await app.getByTestId('track-row').first().dblclick();

    const queueToggle = app.locator('#queue-button');

    await queueToggle.click();

    const row = app.getByTestId('queue-row').first();

    await expect(row).toBeVisible();

    // Played *from the queue*, because a track started from the list
    // leaves `currentIndex` at -1 — so the panel has no current row at
    // all in that flow, which is what made this marker look broken the
    // first time it was checked.
    await row.dblclick();
    await expect(row).toHaveAttribute('aria-current', 'true');

    const marker = await row.evaluate(
      (el) => getComputedStyle(el, '::before').borderLeftWidth,
    );

    // `a11y.22`: a background tint and a text colour were the only two
    // signals, and both are hue (WCAG 1.4.1).
    expect(parseFloat(marker)).toBeGreaterThan(0);

    await queueToggle.click();
    await expect(app.getByTestId('queue-row')).toHaveCount(0);
  });
});
