/**
 * E2E assertion helpers.
 *
 * Used by the per-portal smoke specs to check that a page mounted
 * correctly, didn't bounce the user to a different portal, and didn't
 * spew console errors. These are intentionally narrow — they're the
 * floor, not the ceiling. Per-page business assertions live in the
 * happy-path tests.
 */
import { Page, expect, ConsoleMessage } from '@playwright/test';

/**
 * Track all console errors emitted by the page during a test.
 * Returns a disposer. Pair with `expectNoConsoleErrors` at the end.
 *
 * Filtered out: Vite HMR, React DevTools advisories, and network-level
 * errors (Failed to load resource, 4xx/5xx fetch failures). Those are
 * data-layer issues, not rendering bugs — and smoke tests are about
 * rendering, not data. The data layer is exercised by the flow tests.
 */
const NON_FATAL_PATTERNS = [
  '[vite]',
  'Download the React DevTools',
  'Failed to load resource',      // network fetch failure
  'Failed to load ',              // component-level "Failed to load X" warnings
  'status of 4',                  // 4xx fetch
  'status of 5',                  // 5xx fetch
  'NetworkError',
  'ERR_NETWORK',
  'CONNECTION_REFUSED',
  'AxiosError',
  'paynow',                       // paynow webhook retry noise in dev
  'sync',                         // sync engine retry noise in dev
];

export function captureConsoleErrors(page: Page): { errors: string[]; dispose: () => void } {
  const errors: string[] = [];
  const handler = (msg: ConsoleMessage) => {
    if (msg.type() === 'error') {
      const text = msg.text();
      if (NON_FATAL_PATTERNS.some((p) => text.includes(p))) {
        return;
      }
      errors.push(text);
    }
  };
  page.on('console', handler);
  return {
    errors,
    dispose: () => page.off('console', handler),
  };
}

export function expectNoConsoleErrors(errors: string[]): void {
  if (errors.length > 0) {
    throw new Error(
      `Page produced ${errors.length} console error(s):\n  - ${errors.join('\n  - ')}`
    );
  }
}

/**
 * Check that the page did not redirect to /login (auth failure) or to a
 * different role's dashboard (RBAC misconfig). Useful as a sanity gate
 * before deeper assertions.
 */
export async function expectNoAuthBounce(page: Page, expectedPath: string): Promise<void> {
  const url = new URL(page.url());
  if (url.pathname === '/login') {
    throw new Error(`Expected to land on ${expectedPath} but was redirected to /login (auth failure)`);
  }
  if (url.pathname.startsWith('/login')) {
    throw new Error(`Expected ${expectedPath} but got ${url.pathname}`);
  }
}

/**
 * Assert the page has at least one of the given heading / known text
 * fragments. This is the smoke test's "yes, this rendered" check —
 * looking for a recognizable string beats asserting on a hard-to-predict
 * DOM tree.
 */
export async function expectAnyText(page: Page, fragments: string[]): Promise<void> {
  const body = (await page.locator('body').innerText()).toLowerCase();
  const found = fragments.find((f) => body.includes(f.toLowerCase()));
  if (!found) {
    throw new Error(
      `None of the expected text fragments were found on ${page.url()}.\n` +
      `Looked for: ${fragments.join(', ')}\n` +
      `Page text (first 400 chars): ${body.slice(0, 400)}`
    );
  }
}

/**
 * Wait for the page to settle: no more than 1 in-flight request for
 * 250ms, then resolve. Cheaper than `networkidle` and good enough for
 * SPA route changes.
 */
export async function waitForPageSettled(page: Page): Promise<void> {
  await page.waitForLoadState('domcontentloaded');
  // Give React a beat to mount after route change.
  await page.waitForTimeout(150);
}

/**
 * The page rendered, the URL is what we asked for, and there's no
 * uncaught error. Combine with captureConsoleErrors for a fuller check.
 */
export async function expectPageMounted(
  page: Page,
  expectedPath: string,
  recognizedText: string[]
): Promise<void> {
  await waitForPageSettled(page);
  await expectNoAuthBounce(page, expectedPath);
  await expectAnyText(page, recognizedText);
}
