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

/** the eleven gpt-image-2 sprites the game ships with */
const EXPECTED_SPRITES = [
  'player-idle.png',
  'player-run.png',
  'enemy.png',
  'enemy-spore.png',
  'pickup-wings.png',
  'pickup-dash.png',
  'platform.png',
  'ground.png',
  'background.png',
  'background-grove.png',
  'background-shrine.png',
];

type GameState = {
  scene: string;
  currentRoom: string;
  player: {
    x: number; y: number; hearts: number; onGround: boolean;
    hasDoubleJump: boolean; hasDash: boolean;
  };
  enemy: { x: number; y: number; dir: number };
  enemies: { kind: string; x: number; y: number; dir: number }[];
  pickup: { x: number; y: number; taken: boolean };
  pickups: { id: string; room: string; x: number; y: number; taken: boolean }[];
  broken: string[];
  save: unknown;
  gate: { x: number; w: number; topY: number };
  room: { w: number; h: number };
  fallbackSprites: string[];
};

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

const teleport = (page: Page, x: number, y: number): Promise<void> =>
  page.evaluate(([tx, ty]) => (window as any).__game.teleport(tx, ty), [x, y]);

async function boot(page: Page): Promise<void> {
  await page.goto('/');
  await page.waitForFunction(() => (window as any).__game?.state().scene === 'title', null, {
    timeout: 15_000,
  });
}

async function startPlaying(page: Page): Promise<void> {
  await boot(page);
  await page.keyboard.press('Enter');
  await page.waitForFunction(() => (window as any).__game.state().scene === 'play');
  await page.waitForTimeout(600); // let the fade-in finish and physics settle
}

/** full jump + mid-air second jump while holding right */
async function attemptDoubleJumpRight(page: Page): Promise<void> {
  await page.keyboard.down('ArrowRight');
  await page.keyboard.down('Space');
  await page.waitForTimeout(240);
  await page.keyboard.up('Space');
  await page.waitForTimeout(30);
  await page.keyboard.down('Space');
  await page.waitForTimeout(350);
  await page.keyboard.up('Space');
  await page.waitForTimeout(900);
  await page.keyboard.up('ArrowRight');
}

test('title screen loads', async ({ page }) => {
  await boot(page);
  await expect(page).toHaveTitle(/Lumen Vale/);
  await expect(page.locator('canvas#game')).toBeVisible();
  expect((await state(page)).scene).toBe('title');
});

test('all sprites load — no 404s, no procgen fallbacks', async ({ page }) => {
  const failed: string[] = [];
  const loaded: string[] = [];
  page.on('response', (r) => {
    if (!r.url().includes('/sprites/')) return;
    const name = r.url().split('/').pop()!;
    (r.status() === 200 ? loaded : failed).push(`${name} (${r.status()})`);
  });
  await boot(page);
  expect(failed).toEqual([]);
  for (const sprite of EXPECTED_SPRITES) {
    expect(loaded.join(',')).toContain(sprite);
  }
  expect((await state(page)).fallbackSprites).toEqual([]);
});

test('player moves left and right', async ({ page }) => {
  await startPlaying(page);
  const x0 = (await state(page)).player.x;

  await page.keyboard.down('ArrowRight');
  await page.waitForTimeout(450);
  await page.keyboard.up('ArrowRight');
  const x1 = (await state(page)).player.x;
  expect(x1).toBeGreaterThan(x0 + 40);

  await page.keyboard.down('ArrowLeft');
  await page.waitForTimeout(300);
  await page.keyboard.up('ArrowLeft');
  const x2 = (await state(page)).player.x;
  expect(x2).toBeLessThan(x1 - 20);
});

test('enemy patrols on its own', async ({ page }) => {
  await startPlaying(page);
  const xs: number[] = [];
  for (let i = 0; i < 8; i++) {
    xs.push((await state(page)).enemy.x);
    await page.waitForTimeout(200);
  }
  const span = Math.max(...xs) - Math.min(...xs);
  expect(span).toBeGreaterThan(30); // it is actually walking
  for (const x of xs) {
    expect(x).toBeGreaterThanOrEqual(598); // and staying inside its patrol
    expect(x).toBeLessThanOrEqual(792);
  }
});

test('ability gate blocks the player before the pickup', async ({ page }) => {
  await startPlaying(page);
  const { gate } = await state(page);
  await teleport(page, 845, 470); // on the floor, just left of the gate

  // throw everything at the wall: run + repeated (attempted double) jumps
  for (let i = 0; i < 3; i++) {
    await attemptDoubleJumpRight(page);
  }
  const s = await state(page);
  expect(s.player.hasDoubleJump).toBe(false);
  expect(s.player.x).toBeLessThan(gate.x); // never crossed the wall plane
});

test('touching the relic grants double jump', async ({ page }) => {
  await startPlaying(page);
  const { pickup } = await state(page);
  expect(pickup.taken).toBe(false);
  // stand on the pickup platform — collection happens via real overlap
  await teleport(page, pickup.x, 228);
  await page.waitForFunction(() => (window as any).__game.state().player.hasDoubleJump, null, {
    timeout: 3_000,
  });
  expect((await state(page)).pickup.taken).toBe(true);
});

test('double jump carries the player past the gate', async ({ page }) => {
  await startPlaying(page);
  const { pickup, gate } = await state(page);
  await teleport(page, pickup.x, 228); // collect the wings for real
  await page.waitForFunction(() => (window as any).__game.state().player.hasDoubleJump);

  const gateRight = gate.x + gate.w;
  let passed = false;
  for (let attempt = 0; attempt < 6 && !passed; attempt++) {
    await teleport(page, 845, 470);
    await page.waitForTimeout(150);
    await attemptDoubleJumpRight(page);
    passed = (await state(page)).player.x > gateRight + 10;
  }
  expect(passed).toBe(true);
});

test('enemy contact damages, kills, and respawns the player at the entry', async ({ page }) => {
  await startPlaying(page);

  // feed the player to the beetle until all 3 hearts are gone
  for (let hit = 0; hit < 3; hit++) {
    const before = await state(page);
    if (before.scene !== 'play') break;
    await teleport(page, before.enemy.x, 470);
    await page.waitForFunction(
      (h) => {
        const s = (window as any).__game.state();
        return s.player.hearts < h || s.scene === 'dead';
      },
      before.player.hearts,
      { timeout: 4_000 },
    );
    await page.waitForTimeout(1400); // let i-frames lapse before the next hit
  }

  // death → respawn at room entry with full hearts
  await page.waitForFunction(
    () => {
      const s = (window as any).__game.state();
      return s.scene === 'play' && s.player.hearts === 3 && s.player.x < 120;
    },
    null,
    { timeout: 8_000 },
  );
  const s = await state(page);
  expect(s.player.x).toBeLessThan(120);
  expect(s.player.hearts).toBe(3);
});

// --- v2: rooms, spores, dash, save -----------------------------------------

test('v2: rooms chain A→B→C — spore damage, dash pickup, wall break, save restore', async ({ page }) => {
  test.setTimeout(120_000);
  await startPlaying(page);
  expect((await state(page)).currentRoom).toBe('A');

  // walk off room A's right edge → Crystal Grove
  await teleport(page, 1400, 470);
  await page.keyboard.down('ArrowRight');
  await page.waitForFunction(() => (window as any).__game.state().currentRoom === 'B', null, {
    timeout: 5_000,
  });
  await page.keyboard.up('ArrowRight');
  await page.waitForTimeout(500); // fade-in

  // stand under the drifting spore until a damage cloud connects
  const hearts0 = (await state(page)).player.hearts;
  let hurt = false;
  for (let i = 0; i < 30 && !hurt; i++) {
    const s = await state(page);
    const spore = s.enemies.find((e) => e.kind === 'spore')!;
    await teleport(page, spore.x, 470);
    await page.waitForTimeout(450);
    hurt = (await state(page)).player.hearts < hearts0;
  }
  expect(hurt).toBe(true);

  // collect the EMBER DASH sigil on its high platform
  await teleport(page, 1060, 170);
  await page.waitForFunction(() => (window as any).__game.state().player.hasDash, null, {
    timeout: 3_000,
  });
  expect((await state(page)).pickups.find((p) => p.id === 'dash')!.taken).toBe(true);

  // dash shatters the amber crystal wall, then run on into the shrine
  await teleport(page, 1130, 470);
  await page.waitForTimeout(450); // let pickup hitstop + landing settle
  await page.keyboard.down('ArrowRight');
  await page.waitForTimeout(150);
  await page.keyboard.press('Shift');
  await page.waitForFunction(
    () => (window as any).__game.state().broken.includes('gate-B'),
    null,
    { timeout: 3_000 },
  );
  await page.waitForFunction(() => (window as any).__game.state().currentRoom === 'C', null, {
    timeout: 6_000,
  });
  await page.keyboard.up('ArrowRight');

  const s2 = await state(page);
  expect(s2.currentRoom).toBe('C');
  expect(s2.player.hasDash).toBe(true);

  // save state survives a reload: boot lands back in the shrine with the dash
  await page.reload();
  await page.waitForFunction(() => (window as any).__game?.state().scene === 'title', null, {
    timeout: 15_000,
  });
  expect((await state(page)).save).not.toBeNull();
  await page.keyboard.press('Enter');
  await page.waitForFunction(() => (window as any).__game.state().scene === 'play');
  const s3 = await state(page);
  expect(s3.currentRoom).toBe('C');
  expect(s3.player.hasDash).toBe(true);
});

test('v2: gate without dash — the crystal wall stops a running double-jumper', async ({ page }) => {
  await startPlaying(page);
  // sneak straight to room B with wings but no dash
  await teleport(page, 1400, 470);
  await page.keyboard.down('ArrowRight');
  await page.waitForFunction(() => (window as any).__game.state().currentRoom === 'B');
  await page.keyboard.up('ArrowRight');
  await page.waitForTimeout(400);

  await teleport(page, 1150, 470);
  for (let i = 0; i < 2; i++) {
    await attemptDoubleJumpRight(page);
  }
  const s = await state(page);
  expect(s.currentRoom).toBe('B');
  expect(s.player.x).toBeLessThan(1240); // never crossed the crystal wall plane
  expect(s.broken).toEqual([]);
});

test('v2: RESET SAVE button clears the save from the title screen', async ({ page }) => {
  await startPlaying(page);
  // crossing into room B writes a save
  await teleport(page, 1400, 470);
  await page.keyboard.down('ArrowRight');
  await page.waitForFunction(() => (window as any).__game.state().currentRoom === 'B');
  await page.keyboard.up('ArrowRight');

  await page.reload();
  await page.waitForFunction(() => (window as any).__game?.state().scene === 'title', null, {
    timeout: 15_000,
  });
  expect((await state(page)).save).not.toBeNull();

  // click the RESET SAVE button (canvas-space 872,39 — viewport matches canvas 1:1)
  const box = (await page.locator('canvas#game').boundingBox())!;
  await page.mouse.click(box.x + (872 / 960) * box.width, box.y + (39 / 540) * box.height);
  await page.waitForFunction(() => (window as any).__game.state().save === null, null, {
    timeout: 3_000,
  });

  // a fresh run starts back in room A without abilities
  await page.keyboard.press('Enter');
  await page.waitForFunction(() => (window as any).__game.state().scene === 'play');
  const s = await state(page);
  expect(s.currentRoom).toBe('A');
  expect(s.player.hasDash).toBe(false);
  expect(s.player.hasDoubleJump).toBe(false);
});
