// ui.spec.mjs — browser regression gate for the panel controller UI.
// Runs with @playwright/test:  npx playwright test ui.spec.mjs
//
// Requires a LOCALLY RUNNING controller (see ../../memory/build.md).
//   BASE_URL        controller origin (default http://127.0.0.1:8080)
//   TEST_EMAIL      panel account email    \  needed for the authenticated
//   TEST_PASSWORD   panel account password /  tests; without them those
//                                             tests self-skip (annotated).
//
// Anything that would need a live agent (server cards with real data,
// consoles, etc.) is deliberately NOT asserted — those checks self-skip.
// The point of this suite is the 2026-05-08 class of regression: the page
// serves, the inline JS boots with zero console errors, the root route's
// panel_ui cookie logic doesn't loop, and the command-center chrome
// (palette, hero stats) exists.

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

const BASE = process.env.BASE_URL || 'http://127.0.0.1:8080';
const EMAIL = process.env.TEST_EMAIL || '';
const PASSWORD = process.env.TEST_PASSWORD || '';

// Collect console errors + page errors on a page. Filters nothing: the gate
// is "0 console errors" on first paint, matching the redroid-console house
// boilerplate (page.on('console'/'pageerror') capture).
function captureErrors(page) {
  const errs = [];
  page.on('console', m => { if (m.type() === 'error') errs.push(`[console.error] ${m.text().slice(0, 300)}`); });
  page.on('pageerror', e => errs.push(`[pageerror] ${e.message.slice(0, 300)}`));
  return errs;
}

async function login(page) {
  await page.goto(BASE + '/login');
  await page.fill('#email', EMAIL);
  await page.fill('#password', PASSWORD);
  await page.click('#submit');
  await page.waitForURL(u => !u.pathname.startsWith('/login'), { timeout: 10_000 });
}

test.describe('unauthenticated', () => {
  test('login page renders with its form and 0 console errors', async ({ page }) => {
    const errs = captureErrors(page);
    const resp = await page.goto(BASE + '/login');
    expect(resp.ok()).toBeTruthy();
    await expect(page.locator('#email')).toBeVisible();
    await expect(page.locator('#password')).toBeVisible();
    await expect(page.locator('#submit')).toBeVisible();
    await page.waitForTimeout(500);
    expect(errs, errs.join('\n')).toHaveLength(0);
  });

  test('root without session 302s to /login (no loop)', async ({ page }) => {
    await page.goto(BASE + '/');
    await expect(page).toHaveURL(/\/login/);
  });
});

test.describe('authenticated', () => {
  test.skip(!EMAIL || !PASSWORD,
    'TEST_EMAIL / TEST_PASSWORD not set — authenticated UI checks skipped');

  test('root serves new.html by default, boots with 0 console errors', async ({ page }, testInfo) => {
    const errs = captureErrors(page);
    await login(page);
    await page.goto(BASE + '/');
    // new.html markers: hero-stat tiles + command-palette hint in the topbar.
    await expect(page.locator('#stat-running')).toBeAttached();
    await expect(page.locator('#topbar-cmdk')).toBeAttached();
    await page.waitForTimeout(1500); // let the boot JS run
    // WebSocket/agent-dependent noise would need a live agent — annotate
    // rather than fail on errors that are clearly missing-agent-shaped.
    const agentish = errs.filter(e => /websocket|ws:|agent|ECONNREFUSED/i.test(e));
    const hard = errs.filter(e => !agentish.includes(e));
    if (agentish.length) {
      testInfo.annotations.push({ type: 'needs-live-agent', description: agentish.join(' | ') });
    }
    expect(hard, hard.join('\n')).toHaveLength(0);
  });

  test('panel_ui=classic cookie serves index.html with no redirect loop', async ({ browser }) => {
    const ctx = await browser.newContext();
    const page = await ctx.newPage();
    await login(page);
    await ctx.addCookies([{
      name: 'panel_ui', value: 'classic',
      url: BASE,
    }]);
    // The 2026-05-31 gotcha: FileServer canonicalization made "/" 301-loop
    // for authenticated classic users (ERR_TOO_MANY_REDIRECTS). A successful
    // goto with a 200 final response proves no loop.
    const resp = await page.goto(BASE + '/');
    expect(resp.status()).toBe(200);
    // Classic UI must NOT show the new-brand hero-stat tiles.
    await expect(page.locator('#stat-running')).toHaveCount(0);
    await ctx.close();
  });

  test('command palette opens on Ctrl+K and via the topbar hint', async ({ page }) => {
    await login(page);
    await page.goto(BASE + '/');
    const modal = page.locator('#cmdk-modal');
    await expect(modal).toBeHidden();
    await page.keyboard.press('Control+KeyK');
    await expect(modal).toBeVisible();
    await expect(page.locator('#cmdk-input')).toBeFocused();
    await page.keyboard.press('Escape');
    await expect(modal).toBeHidden();
    await page.click('#topbar-cmdk');
    await expect(modal).toBeVisible();
  });

  test('hero-stat element IDs present', async ({ page }) => {
    await login(page);
    await page.goto(BASE + '/');
    for (const id of ['stat-running', 'stat-players', 'stat-agents', 'stat-cpu',
                      'stat-running-hint', 'stat-players-hint', 'stat-agents-hint', 'stat-cpu-hint',
                      'status-dot', 'status']) {
      await expect(page.locator('#' + id), `#${id} missing from new.html DOM`).toBeAttached();
    }
  });
});
