/**
 * EL3AB UI audit.
 *
 * Drives the real app in a phone-sized browser, walks the scenes, and reports
 * the layout problems that are measurable rather than a matter of taste:
 *
 *   - horizontal overflow (the page scrolling sideways on a phone)
 *   - tap targets below the 44x44 minimum
 *   - text below a legible size
 *   - content clipped by the viewport or by its own container
 *   - elements colliding or overlapping
 *   - hardcoded pixel widths that cannot fit the narrowest phone
 *
 * It screenshots each scene at three real phone widths and writes a JSON report
 * beside the images.
 *
 *   node tools/ui-audit.mjs [--base <url>] [--out <dir>] [--only scene1,scene2]
 */
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';

const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';

const args = process.argv.slice(2);
const argOf = (n, d) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : d; };
const BASE = argOf('--base', 'https://el3ab-player.caprover.al-arcade.com');
const OUT = argOf('--out', './ui-audit');
const ONLY = argOf('--only', '')?.split(',').filter(Boolean);

// Real devices, smallest first. The narrow one is where spacing bugs show up.
const DEVICES = [
  { name: 'small',  width: 360, height: 640 },   // Galaxy A-series / older Android
  { name: 'iphone', width: 390, height: 844 },   // iPhone 13/14/15
  { name: 'large',  width: 430, height: 932 },   // iPhone Pro Max
];

// Scenes reachable without a live opponent. `world` switches the bottom nav.
const SCENES = [
  { id: 'play',              world: 'play' },
  { id: 'rank',              world: 'rank' },
  { id: 'social',            world: 'social' },
  { id: 'tournaments',       world: 'tournaments' },
  { id: 'profile',           world: 'profile' },
  { id: 'play-time-select',  push: ['play-time-select', { game: 'chess', mode: 'bot' }] },
  { id: 'play-bot-select',   push: ['play-bot-select', { game: 'chess' }] },
  { id: 'chess-game-bot',    push: ['chess-game', { mode: 'bot', botId: 'amina', timeControl: 'rapid_10_0' }], settle: 3000 },
  { id: 'ludo-game-bot',     push: ['ludo-game', { mode: 'bot' }], settle: 3000 },
  { id: 'domino-game-bot',   push: ['domino-game', { mode: 'bot' }], settle: 3000 },
  { id: 'backgammon-bot',    push: ['backgammon-game', { mode: 'bot' }], settle: 3000 },
  { id: 'tournaments-hub',   push: ['tournaments-hub', {}] },
  { id: 'leaderboard',       push: ['leaderboard', {}] },
  { id: 'friends',           push: ['friends', {}] },
  { id: 'shop-browse',       push: ['shop-browse', {}] },
  { id: 'achievements',      push: ['achievements', {}] },
  { id: 'profile-settings',  push: ['profile-settings', {}] },
  { id: 'puzzles',           push: ['puzzles', {}] },
  { id: 'daily-reward',      push: ['daily-reward', {}] },
];

/** Runs inside the page. Returns measurable layout defects for the current view. */
const AUDIT_FN = () => {
  const vw = window.innerWidth, vh = window.innerHeight;
  const issues = [];
  const seen = new Set();

  const describe = (el) => {
    const id = el.id ? '#' + el.id : '';
    const cls = typeof el.className === 'string' && el.className
      ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '';
    const txt = (el.textContent || '').trim().slice(0, 24);
    return `${el.tagName.toLowerCase()}${id}${cls}${txt ? ` "${txt}"` : ''}`;
  };
  const push = (type, el, detail) => {
    const key = type + '|' + describe(el) + '|' + detail;
    if (seen.has(key)) return;
    seen.add(key);
    issues.push({ type, el: describe(el), detail });
  };

  // 1. The document must never scroll sideways on a phone.
  const de = document.documentElement;
  if (de.scrollWidth > vw + 1) {
    issues.push({ type: 'page-overflow-x', el: 'document', detail: `scrollWidth ${de.scrollWidth} > viewport ${vw}` });
  }

  const isControl = (n) => n instanceof Element
    && (n.matches('button,a[href],[role="button"],input,select,textarea')
        || getComputedStyle(n).cursor === 'pointer');

  // A carousel or chip row scrolls its children out of view on purpose.
  const inScroller = (n) => {
    for (let p = n.parentElement; p && p !== document.body; p = p.parentElement) {
      const o = getComputedStyle(p).overflowX;
      if (o === 'auto' || o === 'scroll') return true;
    }
    return false;
  };

  const all = Array.from(document.querySelectorAll('body *'));
  for (const el of all) {
    const cs = getComputedStyle(el);
    if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') continue;
    const r = el.getBoundingClientRect();
    if (r.width === 0 && r.height === 0) continue;

    // 2. Anything wider than the screen.
    if (r.width > vw + 1 && cs.position !== 'fixed') {
      push('wider-than-screen', el, `${Math.round(r.width)}px wide vs ${vw}px screen`);
    }

    // 3. Sticking out past either edge.
    if (!inScroller(el)) {
      if (r.left < -1) push('clipped-start', el, `left ${Math.round(r.left)}px`);
      if (r.right > vw + 1) push('clipped-end', el, `right ${Math.round(r.right)}px past ${vw}px`);
    }

    // 4. Tap targets. 44x44 is the documented minimum on both platforms.
    //    Only the OUTERMOST control counts: a 20px label inside a 48px button is
    //    not a small target, and counting it buries the real ones.
    if (isControl(el)) {
      let ancestorIsControl = false;
      for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) {
        if (isControl(p)) { ancestorIsControl = true; break; }
      }
      if (!ancestorIsControl && (r.height < 44 || r.width < 44)) {
        push('tap-target', el, `${Math.round(r.width)}x${Math.round(r.height)} (min 44x44)`);
      }
    }

    // 5. Text too small to read on a phone.
    const hasOwnText = Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.trim());
    if (hasOwnText) {
      const fs = parseFloat(cs.fontSize);
      if (fs && fs < 11) push('tiny-text', el, `${fs}px`);
    }

    // 6. Content taller/wider than its own box with no way to scroll to it.
    const clipsY = el.scrollHeight > el.clientHeight + 2 && cs.overflowY === 'hidden' && el.clientHeight > 0;
    const clipsX = el.scrollWidth > el.clientWidth + 2 && cs.overflowX === 'hidden' && el.clientWidth > 0;
    if (clipsY) push('clipped-content-y', el, `content ${el.scrollHeight}px in ${el.clientHeight}px box`);
    if (clipsX) push('clipped-content-x', el, `content ${el.scrollWidth}px in ${el.clientWidth}px box`);

  }

  return {
    viewport: { w: vw, h: vh },
    scrollWidth: de.scrollWidth,
    scrollHeight: de.scrollHeight,
    issues,
  };
};

// Every guest sign-in creates a real account. Track them and remove them at the
// end, so repeated audit runs do not litter the database with throwaway players.
const SB = 'https://safe-supabase-kong.caprover.al-arcade.com';
const SK = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4';
const createdGuests = new Set();

async function removeGuests() {
  for (const id of createdGuests) {
    for (const path of [`chat_messages?sender_id=eq.${id}`, `matchmaking_queue?player_id=eq.${id}`,
                        `mp_log?player_id=eq.${id}`, `matches?white_player_id=eq.${id}`,
                        `matches?black_player_id=eq.${id}`, `player_achievements?player_id=eq.${id}`,
                        `profiles?id=eq.${id}`]) {
      await fetch(`${SB}/rest/v1/${path}`, { method: 'DELETE', headers: { apikey: SK, Authorization: `Bearer ${SK}` } }).catch(() => {});
    }
    await fetch(`${SB}/auth/v1/admin/users/${id}`, { method: 'DELETE', headers: { apikey: SK, Authorization: `Bearer ${SK}` } }).catch(() => {});
  }
  if (createdGuests.size) console.log(`\ncleaned up ${createdGuests.size} throwaway guest account(s)`);
}

async function guestLogin(page) {
  await page.goto(BASE + '/', { waitUntil: 'networkidle2', timeout: 60000 });
  await new Promise(r => setTimeout(r, 1500));

  // Already signed in from a previous run?
  const needsLogin = await page.evaluate(() => !JSON.parse(localStorage.getItem('el3ab_state')||'{}')?.auth?.token
    && !!document.body.innerText.match(/كضيف|Guest/));
  if (!needsLogin) return true;

  const clicked = await page.evaluate(() => {
    const btn = Array.from(document.querySelectorAll('button,a'))
      .find(b => /كضيف|guest/i.test(b.textContent || ''));
    if (btn) { btn.click(); return true; }
    return false;
  });
  if (!clicked) return false;
  await new Promise(r => setTimeout(r, 4000));

  const id = await page.evaluate(() => {
    try { return JSON.parse(localStorage.getItem('el3ab_state') || '{}')?.auth?.userId || null; }
    catch (e) { return null; }
  });
  if (id) createdGuests.add(id);
  return true;
}

async function goToScene(page, scene) {
  await page.evaluate(async (s) => {
    const mod = await import('/public/js/core/scene.js');
    if (s.world) { mod.switchWorld(s.world, true); return; }
    if (s.push) mod.push(s.push[0], s.push[1] || {});
  }, scene);
  await new Promise(r => setTimeout(r, scene.settle || 1400));
}

// ---------------------------------------------------------------------------
fs.mkdirSync(OUT, { recursive: true });
const browser = await puppeteer.launch({
  executablePath: CHROME, headless: 'new',
  args: ['--no-sandbox', '--disable-dev-shm-usage', '--font-render-hinting=none'],
});

const report = { base: BASE, when: new Date().toISOString(), devices: {} };
const consoleErrors = [];

for (const device of DEVICES) {
  const page = await browser.newPage();
  page.on('pageerror', e => consoleErrors.push({ device: device.name, error: String(e).slice(0, 200) }));
  page.on('console', m => {
    if (m.type() === 'error') consoleErrors.push({ device: device.name, error: m.text().slice(0, 200) });
  });
  await page.setViewport({ ...device, deviceScaleFactor: 2, isMobile: true, hasTouch: true });

  const ok = await guestLogin(page);
  if (!ok) { console.log(`[${device.name}] could not sign in as guest`); await page.close(); continue; }

  report.devices[device.name] = {};
  for (const scene of SCENES) {
    if (ONLY.length && !ONLY.includes(scene.id)) continue;
    try {
      await goToScene(page, scene);
      const result = await page.evaluate(AUDIT_FN);
      const file = path.join(OUT, `${device.name}-${scene.id}.png`);
      await page.screenshot({ path: file });
      report.devices[device.name][scene.id] = result;
      const n = result.issues.length;
      console.log(`[${device.name}] ${scene.id.padEnd(20)} ${String(n).padStart(3)} issue(s)`);
    } catch (e) {
      console.log(`[${device.name}] ${scene.id.padEnd(20)} ERROR ${e.message.slice(0, 80)}`);
      report.devices[device.name][scene.id] = { error: e.message };
    }
  }
  await page.close();
}
report.consoleErrors = consoleErrors;
await browser.close();
await removeGuests();

fs.writeFileSync(path.join(OUT, 'report.json'), JSON.stringify(report, null, 2));

// ---- summary ----
const byType = {};
for (const [dev, scenes] of Object.entries(report.devices)) {
  for (const [scene, r] of Object.entries(scenes)) {
    for (const i of (r.issues || [])) {
      byType[i.type] ??= [];
      byType[i.type].push(`${dev}/${scene}: ${i.el} — ${i.detail}`);
    }
  }
}
console.log('\n=== issues by type ===');
for (const [type, list] of Object.entries(byType).sort((a, b) => b[1].length - a[1].length)) {
  console.log(`\n${type}  (${list.length})`);
  for (const l of list.slice(0, 8)) console.log('   ' + l);
  if (list.length > 8) console.log(`   … ${list.length - 8} more`);
}
if (consoleErrors.length) {
  console.log(`\n=== console errors (${consoleErrors.length}) ===`);
  const uniq = [...new Set(consoleErrors.map(e => e.error))];
  uniq.slice(0, 12).forEach(e => console.log('   ' + e));
}
console.log(`\nreport: ${path.join(OUT, 'report.json')}`);
