import { test, expect } from '../support/fixtures.js';

/**
 * Back is the platform's, and the app has to have somewhere for it to
 * go (reported from a device: "the Android back button does not
 * navigate back in the app").
 *
 * The scaffold's `MainActivity.onBackPressed` asks `webView.canGoBack()`
 * and finishes the activity otherwise. This app never touched
 * `history`, so that was always false and back quit from any depth. A
 * navigation is a history entry now, which is why this is assertable
 * here at all: `page.goBack()` is the same `popstate` the phone's
 * gesture produces, so the browser tier can answer a question that
 * otherwise needs a device.
 *
 * What it cannot answer is whether Android's *gesture* reaches the
 * WebView, which is between the OS and the scaffold.
 *
 * **And `data-active-view` is not the behaviour.** Every assertion here
 * used to be that attribute, which the shell sets on every path
 * including `_isBack` — so this file was green throughout #72, in
 * which both navs highlighted the view the user had just *left*. The
 * shell's own bookkeeping was the one thing that was already right;
 * what a person sees is `aria-current`, and that is asserted below as
 * well. This is the same trap `layout-overflow.spec.ts` set for #69: a
 * spec named for the behaviour, measuring the plumbing.
 */
type Page = import('@playwright/test').Page;

const activeView = (page: Page) =>
  page.getByTestId('main-content');

/** A common phone, where the bottom bar is the primary navigation. */
const PHONE = { width: 390, height: 844 };

/**
 * The nav item for a destination, in whichever navigation is on screen.
 *
 * Both navs carry a button named `Albums`, and only one of them is ever
 * in the accessibility tree — the other is `display: none` — so the
 * role query resolves to the one the user can see at this viewport.
 * That is the point: the highlight has to be right in both, and #72 was
 * two different-looking symptoms of one cause.
 */
const navItem = (page: Page, label: string) =>
  page.getByRole('button', { name: label, exact: true });

/**
 * `aria-current="page"` is the accessible fact and the assertion worth
 * making; `.active` is a class and could be restyled without breaking
 * anything real.
 */
async function expectHighlighted(page: Page, label: string): Promise<void> {
  await expect(navItem(page, label)).toHaveAttribute('aria-current', 'page');
}

async function expectNotHighlighted(page: Page, label: string): Promise<void> {
  await expect(navItem(page, label)).toHaveAttribute('aria-current', 'false');
}

/**
 * Open an artist's detail view, which is the deepest ordinary route.
 *
 * A library artist opens `explore-artist-details` -- the catalog panel
 * standing in for a library one, as `explore-link.ts` describes -- and
 * the view name follows the component, not the source of the click.
 */
async function openAnArtist(app: Page): Promise<void> {
  await app.getByTestId('nav-artists').click();
  await expect(activeView(app)).toHaveAttribute('data-active-view', 'artists');

  // A card, by the name on it: the grid is virtualized and positioned
  // by transform, so a click at coordinates is a click at whatever
  // happens to be there.
  await app.locator('artists-view').getByText('Aurora Fields').first().click();
  await expect(activeView(app)).toHaveAttribute(
    'data-active-view',
    'explore-artist-details',
  );
}

/**
 * The global back/forward control (#6).
 *
 * It is desktop chrome — hidden below 900px, where the sidebar has
 * already given up its labels — so these set a desktop viewport
 * explicitly rather than trusting the runner's default.
 */
const DESKTOP = { width: 1280, height: 800 };

const backButton = (page: Page) =>
  page.locator('nav-history').getByRole('button', { name: 'Back' });

const forwardButton = (page: Page) =>
  page.locator('nav-history').getByRole('button', { name: 'Forward' });

test.describe('global back and forward', () => {
  test.beforeEach(async ({ app }) => {
    await app.setViewportSize(DESKTOP);
  });

  test('offers nothing at launch, in either direction', async ({ app }) => {
    // The launch entry is *replaced*, not pushed, so there is nothing
    // of ours behind it — and a Back button that is live at the root
    // is a press that does nothing on desktop and, on Android, the
    // press that should have exited the app (#142). This assertion is
    // what pins that: it failed before the launch navigation stopped
    // recording two entries.
    await expect(backButton(app)).toBeDisabled();
    await expect(forwardButton(app)).toBeDisabled();
  });

  test('walks the history in both directions, and says which are available', async ({
    app,
  }) => {
    await app.getByTestId('nav-albums').click();
    await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
    await expect(backButton(app)).toBeEnabled();
    await expect(forwardButton(app)).toBeDisabled();

    await app.getByTestId('nav-tracks').click();
    await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');

    await backButton(app).click();

    await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
    // Standing in the middle of the list: both directions live, which
    // is the state a single depth counter cannot express.
    await expect(backButton(app)).toBeEnabled();
    await expect(forwardButton(app)).toBeEnabled();

    await forwardButton(app).click();

    await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');
    await expect(forwardButton(app)).toBeDisabled();
  });

  test('reaches the detail view a tab click left behind', async ({ app }) => {
    // The report, exactly: the album is one entry away the whole time,
    // and before this control the only way back to it was a button
    // that had gone off screen with the view it belonged to.
    await app.getByTestId('nav-artists').click();
    await openAnArtist(app);

    await app.getByTestId('nav-tracks').click();
    await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');

    await backButton(app).click();

    await expect(activeView(app)).toHaveAttribute(
      'data-active-view',
      'explore-artist-details',
    );
  });

  test('drops the forward list when the user navigates from the middle', async ({
    app,
  }) => {
    await app.getByTestId('nav-albums').click();
    await app.getByTestId('nav-tracks').click();
    await backButton(app).click();
    await expect(forwardButton(app)).toBeEnabled();

    // A browser truncates here, and so does this: what was ahead is no
    // longer reachable, and a Forward button still offering it would
    // be pointing at an entry that has been overwritten.
    await app.getByTestId('nav-genres').click();

    await expect(activeView(app)).toHaveAttribute('data-active-view', 'genres');
    await expect(forwardButton(app)).toBeDisabled();
    await expect(backButton(app)).toBeEnabled();
  });

  test('is absent below the desktop band, where nothing needs it', async ({
    app,
  }) => {
    // Alt+Left/Right survive at every width, the detail views keep
    // their own back buttons and the phone has the platform's gesture
    // — so this is a control standing down, not an action becoming
    // unreachable. It is hidden at 899 because the top bar is what
    // runs out of room first below 900 (#143).
    await app.setViewportSize({ width: 899, height: 600 });
    await expect(app.locator('nav-history')).toBeHidden();

    await app.setViewportSize({ width: 390, height: 844 });
    await expect(app.locator('nav-history')).toBeHidden();
  });
});

test.describe('the back gesture', () => {
  test('leaves a detail view for the view it was opened from', async ({
    app,
  }) => {
    await openAnArtist(app);

    await app.goBack();

    await expect(activeView(app)).toHaveAttribute('data-active-view', 'artists');
  });

  test('walks back through primary views, one press per navigation', async ({
    app,
  }) => {
    await app.getByTestId('nav-tracks').click();
    await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');

    await app.getByTestId('nav-albums').click();
    await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');

    await app.goBack();
    await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');

    // Forward is free once back works, and it is what proves the entry
    // was restored rather than the view merely re-rendered.
    await app.goForward();
    await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
  });

  test('leaves the nav highlighting the view it landed on, not the one it left', async ({
    app,
  }) => {
    await app.getByTestId('nav-albums').click();
    await expectHighlighted(app, 'Albums');

    await app.getByTestId('nav-tracks').click();
    await expectHighlighted(app, 'Tracks');

    await app.goBack();

    // #72, and the half of it the report did not describe: this is
    // desktop, and before the shell published the active view *both*
    // navs stayed on Tracks. An absent highlight reads as a glitch; a
    // confident wrong one is worse, and any back across two primary
    // views produced it.
    await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
    await expectHighlighted(app, 'Albums');
    await expectNotHighlighted(app, 'Tracks');
  });

  test('keeps the parent destination lit while a detail view is open', async ({
    app,
  }) => {
    await app.getByTestId('nav-artists').click();
    await expectHighlighted(app, 'Artists');

    await openAnArtist(app);

    // A detail view is not a destination in either nav, and the user is
    // still inside Artists. `app-sidebar` did this by accident -- it
    // guarded on its own item list, so an unmatched name left the
    // highlight alone -- and that accident is why the sidebar looked
    // right on a detail view while the tab bar lit nothing. This test
    // therefore passed before the fix and is here to keep the rule from
    // being lost while the others are made to pass; the *tab bar's*
    // half of it is the phone test below, which did not.
    await expectHighlighted(app, 'Artists');

    await app.goBack();

    await expectHighlighted(app, 'Artists');
  });

  test('the tab bar survives the same journey on a phone', async ({ app }) => {
    await app.setViewportSize(PHONE);

    // The reported shape: Albums, open an album, press back. The tab
    // bar had a highlight, then no highlight at all, and never got it
    // back — `bottom-nav` took the detail view's name, matched it
    // against no tab, and lit nothing.
    await navItem(app, 'Albums').click();
    await expectHighlighted(app, 'Albums');

    await app.locator('cover-grid').getByText('Glass Harbour').first().click();
    await expect(activeView(app)).toHaveAttribute(
      'data-active-view',
      'explore-album-details',
    );
    await expectHighlighted(app, 'Albums');

    await app.goBack();

    await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
    await expectHighlighted(app, 'Albums');
  });

  test('the drawer sidebar opens on the page you are standing on', async ({
    app,
  }) => {
    await app.setViewportSize(PHONE);

    await navItem(app, 'Tracks').click();
    await expectHighlighted(app, 'Tracks');

    // A third symptom of the same cause, found while measuring #72 and
    // not in the report: `bottom-nav` mounts its `<app-sidebar>` when
    // the drawer opens, so that copy had heard no `navigate` at all and
    // showed its own default — Home, from any page in the app. An event
    // has no answer for a listener that was not there; a store does.
    await navItem(app, 'More').click();

    // The element carrying the testid is the `wa-drawer` host, which
    // always reports hidden -- what is visible is the `<dialog>` in its
    // shadow root -- so the drawer being open is asserted of the
    // sidebar it holds rather than of itself.
    const drawer = app.getByTestId('nav-drawer');

    await expect(drawer.locator('app-sidebar')).toBeVisible();
    await expect(drawer.getByTestId('nav-tracks')).toHaveAttribute(
      'aria-current',
      'page',
    );
    await expect(drawer.getByTestId('nav-home')).toHaveAttribute(
      'aria-current',
      'false',
    );
  });

  test('an in-app back button consumes exactly one entry', async ({ app }) => {
    await app.getByTestId('nav-tracks').click();
    await openAnArtist(app);

    // The detail view's own back button and the phone's gesture are the
    // same press: if each popped its own stack, this would land two
    // navigations back instead of one.
    await app
      .locator('explore-artist-details')
      .getByRole('button', { name: 'Back to explore' })
      .click();

    await expect(activeView(app)).toHaveAttribute('data-active-view', 'artists');

    await app.goBack();

    await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');
  });
});
