/**
 * APHELION e2e — the full discover → unlock → progress loop, in portrait.
 *
 * title → play → move/jump in the vestibule → teleport to the archive →
 * grab the STARGRIP → teleport to the shaft → wall-jump climb (simulated
 * keys) → light the beacon → jump the dome hatch into the orrery → land
 * hits on the boss. Zero console errors throughout.
 */
import { test, expect, type Page } from '@playwright/test';

type GameState = {
  phase: string;
  room: string;
  hasAbility: boolean;
  hp: number;
  bossHp: number | null;
  bossState: string | null;
  bossX: number | null;
  bossY: number | null;
  beaconLit: boolean;
  checkpoint: { room: string; x: number; y: number };
  player: { x: number; y: number; vy: number; onGround: boolean; wallSliding: number };
  pickup: { x: number; y: number; taken: boolean };
  fallbackSprites: string[];
};

const state = (page: Page) =>
  page.evaluate(() => (window as any).__game.state() as GameState);

const teleport = (page: Page, room: string, x?: number, y?: number) =>
  page.evaluate(
    ([r, px, py]) => (window as any).__game.teleport(r, px, py),
    [room, x, y] as const,
  );

function collectErrors(page: Page): string[] {
  const errors: string[] = [];
  page.on('console', (msg) => {
    if (msg.type() === 'error' && !msg.text().includes('favicon')) errors.push(msg.text());
  });
  page.on('pageerror', (err) => errors.push(String(err)));
  return errors;
}

async function boot(page: Page): Promise<void> {
  await page.goto('/');
  await page.waitForFunction(() => (window as any).__game !== undefined);
  // settle the loading screen
  await page.waitForTimeout(300);
}

/** a real held jump — instant press() gets clipped by the variable-jump cut */
async function jump(page: Page, holdMs = 180): Promise<void> {
  await page.keyboard.down('Space');
  await page.waitForTimeout(holdMs);
  await page.keyboard.up('Space');
}

test('full loop: title → ability → gate → boss', async ({ page }) => {
  const errors = collectErrors(page);
  await boot(page);

  // --- title ---------------------------------------------------------------
  expect((await state(page)).phase).toBe('title');
  await page.keyboard.press('Space');
  await page.waitForFunction(() => (window as any).__game.phase === 'play');
  expect((await state(page)).room).toBe('vestibule');

  // --- room 1: move + jump -------------------------------------------------
  const before = await state(page);
  await page.keyboard.down('KeyD');
  await page.waitForTimeout(450);
  await page.keyboard.up('KeyD');
  const afterMove = await state(page);
  expect(afterMove.player.x).toBeGreaterThan(before.player.x + 40);

  await page.keyboard.down('Space');
  await page.waitForTimeout(200);
  const midJump = await state(page);
  await page.keyboard.up('Space');
  expect(midJump.player.y).toBeLessThan(afterMove.player.y - 30);
  // wait for landing
  await page.waitForFunction(() => (window as any).__game.state().player.onGround);

  // --- room 2: the unlock --------------------------------------------------
  await teleport(page, 'archive');
  expect((await state(page)).room).toBe('archive');
  expect((await state(page)).hasAbility).toBe(false);

  // drop onto the dais next to the STARGRIP and walk into it
  await teleport(page, 'archive', 160, 400);
  await page.keyboard.down('KeyD');
  await page.waitForFunction(() => (window as any).__game.hasAbility, undefined, {
    timeout: 5000,
  });
  await page.keyboard.up('KeyD');

  // progression persisted on pickup
  const saved = await page.evaluate(() =>
    JSON.parse(localStorage.getItem('aphelion-save-v1') ?? 'null'),
  );
  expect(saved?.ability).toBe(true);

  // --- room 3: the gate (wall-jump climb, simulated keys) -------------------
  await teleport(page, 'shaft');
  expect((await state(page)).room).toBe('shaft');
  const startY = (await state(page)).player.y; // 1624, atrium floor

  // run right under the shaft, jump onto the right wall face
  await page.keyboard.down('KeyD');
  await page.waitForFunction(() => (window as any).__game.state().player.x > 240);

  // ping-pong: whenever we grip a wall, kick off and steer at the other one
  let held: 'KeyA' | 'KeyD' = 'KeyD';
  let climbed = false;
  for (let i = 0; i < 120; i++) {
    const s = await state(page);
    if (s.player.y < startY - 500) {
      climbed = true;
      break;
    }
    if (s.player.onGround) {
      await jump(page, 160); // hop back onto a wall
    } else if (s.player.wallSliding !== 0) {
      await page.keyboard.up(held); // swap steering, then kick
      held = held === 'KeyD' ? 'KeyA' : 'KeyD';
      await page.keyboard.down(held);
      await jump(page, 160);
    } else {
      await page.waitForTimeout(70);
    }
  }
  await page.keyboard.up(held);
  expect(climbed, 'wall-jump should climb the shaft by 500+px').toBe(true);

  // --- the beacon checkpoint + the dome hatch -------------------------------
  await teleport(page, 'shaft', 205, 318);
  await page.waitForFunction(() => (window as any).__game.state().beaconLit);
  const cp = (await state(page)).checkpoint;
  expect(cp.room).toBe('shaft');

  // jump up through the hatch into the boss room
  for (let i = 0; i < 10; i++) {
    await jump(page, 280);
    await page.waitForTimeout(250);
    if ((await state(page)).room === 'orrery') break;
  }
  expect((await state(page)).room).toBe('orrery');

  // --- the boss takes damage -------------------------------------------------
  await page.waitForFunction(() => (window as any).__game.bossHp !== null);
  let minBossHp = 10;
  let heldB: 'KeyA' | 'KeyD' | null = null;
  for (let i = 0; i < 300 && minBossHp > 8; i++) {
    await page.waitForTimeout(100);
    const s = await state(page);
    if (s.bossHp !== null && s.bossHp < minBossHp) minBossHp = s.bossHp;
    if (s.phase !== 'play' || s.bossX === null || s.bossY === null) continue;
    // chase the warden whenever it swoops low; swing when in reach
    if (s.bossY > 520) {
      const dx = s.bossX - s.player.x;
      const want: 'KeyA' | 'KeyD' | null = dx > 50 ? 'KeyD' : dx < -50 ? 'KeyA' : null;
      if (want !== heldB) {
        if (heldB) await page.keyboard.up(heldB);
        if (want) await page.keyboard.down(want);
        heldB = want;
      }
      if (Math.abs(dx) < 95) await page.keyboard.press('KeyJ');
    } else if (heldB) {
      await page.keyboard.up(heldB);
      heldB = null;
    }
  }
  if (heldB) await page.keyboard.up(heldB);
  expect(minBossHp, 'the boss should take at least 2 hits').toBeLessThanOrEqual(8);

  // --- hygiene ---------------------------------------------------------------
  expect((await state(page)).fallbackSprites, 'all art should be generated').toEqual([]);
  expect(errors, 'no console errors').toEqual([]);
});

test('the vestibule gate holds without the ability', async ({ page }) => {
  const errors = collectErrors(page);
  await boot(page);
  await page.keyboard.press('Space');
  await page.waitForFunction(() => (window as any).__game.phase === 'play');

  // drop onto the balcony at the chimney mouth, then try to climb bare-handed
  await teleport(page, 'vestibule', 210, 840);
  let best = 9999;
  let held: 'KeyA' | 'KeyD' = 'KeyA';
  await page.keyboard.down(held);
  for (let i = 0; i < 40; i++) {
    await page.waitForTimeout(90);
    const s = await state(page);
    best = Math.min(best, s.player.y);
    await page.keyboard.press('Space');
    if (i % 4 === 3) {
      await page.keyboard.up(held);
      held = held === 'KeyD' ? 'KeyA' : 'KeyD';
      await page.keyboard.down(held);
    }
  }
  await page.keyboard.up(held);
  // a single jump from the balcony (842) reaches ~726; the door ledge is 392.
  // without STARGRIP the player must never get anywhere near it.
  expect(best).toBeGreaterThan(650);
  expect((await state(page)).room).toBe('vestibule');
  expect(errors).toEqual([]);
});

test('save restores room + ability; title reset clears it', async ({ page }) => {
  await boot(page);
  await page.keyboard.press('Space');
  await page.waitForFunction(() => (window as any).__game.phase === 'play');
  await teleport(page, 'archive', 160, 400);
  await page.keyboard.down('KeyD');
  await page.waitForFunction(() => (window as any).__game.hasAbility);
  await page.keyboard.up('KeyD');

  // reload: the save should put us back in the archive with the grip
  await page.reload();
  await page.waitForFunction(() => (window as any).__game !== undefined);
  await page.waitForTimeout(300);
  await page.keyboard.press('Space');
  await page.waitForFunction(() => (window as any).__game.phase === 'play');
  const s = await state(page);
  expect(s.room).toBe('archive');
  expect(s.hasAbility).toBe(true);

  // back on the title screen, RESET SAVE wipes the run
  await page.reload();
  await page.waitForFunction(() => (window as any).__game !== undefined);
  await page.waitForTimeout(300);
  const reset = await page.evaluate(() => {
    const c = document.getElementById('game') as HTMLCanvasElement;
    const r = c.getBoundingClientRect();
    // RESET_BTN center in canvas coords → client coords
    const x = (390 - 118 + 51) * (r.width / 390) + r.left;
    const y = (16 + 16) * (r.height / 844) + r.top;
    const ev = new PointerEvent('pointerdown', { clientX: x, clientY: y, bubbles: true });
    c.dispatchEvent(ev);
    return true;
  });
  expect(reset).toBe(true);
  await page.waitForTimeout(200);
  const cleared = await page.evaluate(() => localStorage.getItem('aphelion-save-v1'));
  expect(cleared).toBeNull();
});
