import assert from 'node:assert/strict';
import test from 'node:test';
import { createGame, startGame, retryGame, stepGame, damagePlayer, PHYSICS } from '../game/simulation';
import { EMPTY_INPUT, WIDTH } from '../game/types';
import type { GameInput, GameState, Shot } from '../game/types';

function game() { const s = createGame(); startGame(s); s.enemies = []; s.level.hazards = []; return s; }
function step(s: GameState, input: Partial<GameInput> = {}, frames = 1) {
  for (let i = 0; i < frames; i++) stepGame(s, { ...EMPTY_INPUT, ...input, ...(i ? { jumpPressed: false, jumpReleased: false, shootPressed: false, shootReleased: false, dashPressed: false } : {}) });
}
function ground(s: GameState) { step(s, {}, 2); assert.equal(s.player.grounded, true); }
function shot(x: number, y: number, overrides: Partial<Shot> = {}): Shot {
  return { id: 999, x, y, w: 12, h: 12, vx: 0, vy: 0, damage: 8, charged: 2, owner: 'player', life: 1, ...overrides };
}

test('gravity lands the player flush on the road without sinking', () => {
  const s = game(); s.player.y = 240;
  step(s, {}, 100);
  assert.equal(s.player.y + s.player.h, 360);
  assert.equal(s.player.vy, 0);
  assert.equal(s.player.grounded, true);
});

test('a solid wall blocks lateral motion and supports wall sliding and wall jumping', () => {
  const s = game();
  s.level.platforms = [{ x: 150, y: 100, w: 50, h: 480, kind: 'metal' }];
  Object.assign(s.player, { x: 120, y: 180, vy: 130 });
  step(s, { right: true }, 20);
  assert.equal(s.player.x + s.player.w, 150);
  assert.equal(s.player.wall, 1);
  assert.ok(s.player.vy <= PHYSICS.wallSlideSpeed + 1);
  step(s, { right: true, jump: true, jumpPressed: true });
  assert.ok(s.player.vx < 0 && s.player.vy < 0);
  assert.ok(s.player.x + s.player.w < 150);
});

test('holding jump reaches a higher apex than releasing early', () => {
  const apex = (release: number) => {
    const s = game(); ground(s); let min = s.player.y;
    for (let i = 0; i < 70; i++) {
      step(s, { jump: i < release, jumpPressed: i === 0, jumpReleased: i === release });
      min = Math.min(min, s.player.y);
    }
    return min;
  };
  assert.ok(apex(40) < apex(5) - 15);
});

test('coyote time allows a jump just after walking off an edge', () => {
  const s = game(); s.level.platforms = [{ x: 0, y: 360, w: 110, h: 200, kind: 'road' }];
  ground(s);
  for (let i = 0; i < 90 && s.player.grounded; i++) step(s, { right: true });
  assert.equal(s.player.grounded, false);
  assert.ok(s.player.coyote > 0);
  step(s, { jumpPressed: true, jump: true });
  assert.ok(s.player.vy < -250);
});

test('jump input shortly before landing is buffered', () => {
  const s = game(); Object.assign(s.player, { y: 331, vy: 180, grounded: false });
  step(s, { jumpPressed: true, jump: true });
  step(s, { jump: true });
  assert.ok(s.player.vy < -250);
});

test('air dash is limited until landing and has a finite duration', () => {
  const s = game(); s.player.y = 200;
  step(s, { dashPressed: true });
  assert.equal(s.player.airDashUsed, true);
  assert.ok(s.player.vx > 250);
  step(s, {}, 21);
  assert.equal(s.player.dashTime, 0);
  step(s, { dashPressed: true });
  assert.equal(s.player.dashTime, 0);
  step(s, {}, 90);
  assert.equal(s.player.grounded, true);
  assert.equal(s.player.airDashUsed, false);
  step(s, { dashPressed: true });
  assert.ok(s.player.dashTime > 0);
});

test('tap fires immediately; medium and full charge increase damage', () => {
  for (const [frames, damage] of [[0, 1], [45, 4], [80, 8]]) {
    const s = game(); ground(s);
    step(s, { shoot: true, shootPressed: true });
    assert.equal(s.shots[0].damage, 1);
    if (frames) step(s, { shoot: true }, frames);
    step(s, { shootReleased: true });
    assert.equal(s.shots.at(-1)?.damage, damage);
    assert.equal(s.player.charge, 0);
  }
});

test('shots hit solids instead of travelling through walls', () => {
  const s = game();
  s.level.platforms.push({ x: 200, y: 200, w: 20, h: 200, kind: 'metal' });
  step(s, { shootPressed: true }); step(s, {}, 25);
  assert.equal(s.shots.length, 0);
});

test('damage applies knockback and invulnerability prevents repeated hits', () => {
  const s = game();
  assert.equal(damagePlayer(s, 5, 150), true);
  assert.equal(s.player.hp, 23);
  assert.ok(s.player.vx < 0 && s.player.vy < 0);
  assert.equal(damagePlayer(s, 5, 150), false);
  assert.equal(s.player.hp, 23);
});

test('defeating an enemy awards a kill and a deterministic health drop', () => {
  const s = game();
  const e = { ...s.level.enemies[0], id: 3, x: 170, hp: 1 };
  s.enemies = [e]; s.shots = [shot(e.x, e.y)];
  step(s);
  assert.equal(s.enemies.length, 0);
  assert.equal(s.kills, 1);
  assert.equal(s.pickups.length, 1);
  s.player.hp = 10;
  s.player.x = s.pickups[0].x;
  step(s);
  assert.equal(s.player.hp, 17);
  assert.equal(s.pickups.length, 0);
});

test('crossing a checkpoint restores health and death retries reset the encounter', () => {
  const s = game();
  s.player.x = s.level.checkpoints[1].x + 1; s.player.hp = 8;
  step(s);
  assert.equal(s.checkpoint, 1);
  assert.equal(s.player.hp, 28);
  s.player.invulnerable = 0;
  damagePlayer(s, 40, s.player.x + 50);
  assert.equal(s.mode, 'dead'); assert.equal(s.deaths, 1);
  const elapsed = s.elapsed;
  s.shots = [shot(0, 0)]; s.player.charge = 1.8; s.player.dashTime = 1;
  s.boss.hp = 10;
  retryGame(s);
  assert.equal(s.mode, 'playing'); assert.equal(s.player.x, s.level.checkpoints[1].x);
  assert.equal(s.player.hp, 28); assert.equal(s.player.charge, 0); assert.equal(s.player.dashTime, 0);
  assert.equal(s.shots.length, 0); assert.equal(s.particles.length, 0); assert.equal(s.pickups.length, 0);
  assert.equal(s.enemies.length, s.level.enemies.length);
  assert.equal(s.boss.hp, s.boss.maxHp); assert.equal(s.boss.active, false);
  assert.equal(s.elapsed, elapsed); assert.equal(s.deaths, 1);
});

test('pausing freezes physics, projectiles and the run timer', () => {
  const s = game(); step(s, { shootPressed: true }); s.mode = 'paused';
  const before = JSON.stringify(s);
  step(s, { right: true, jumpPressed: true }, 120);
  assert.equal(JSON.stringify(s), before);
});

test('boss activation locks a single-screen arena and cycles through telegraphed attacks', () => {
  const s = game(); s.player.x = s.level.arena.x + 30;
  step(s);
  assert.equal(s.boss.active, true);
  assert.equal(s.level.arena.w, WIDTH);
  assert.equal(s.camera.x, s.level.arena.x);
  const seen = new Set<string>();
  for (let i = 0; i < 1300; i++) { s.player.invulnerable = 5; step(s); seen.add(s.boss.phase); }
  for (const phase of ['windup-dash', 'dash', 'windup-shot', 'shoot', 'windup-slam', 'slam', 'recover']) assert.ok(seen.has(phase), `Missing phase ${phase}`);
});

test('the enraged boss has a shorter warning while preserving a reaction window', () => {
  const duration = (hp: number) => {
    const s = game(); s.player.x = s.level.arena.x + 30; step(s);
    s.boss.hp = hp; s.boss.phase = 'idle'; s.boss.timer = 0;
    step(s);
    assert.equal(s.boss.phase, 'windup-dash');
    return s.boss.timer;
  };
  assert.ok(duration(50) < duration(120));
  assert.ok(duration(50) >= 0.6);
});

test('defeating the boss wins; replay resets the entire run', () => {
  const s = game(); s.player.x = s.level.arena.x + 30; step(s);
  s.boss.hp = 1; s.shots = [shot(s.boss.x + 12, s.boss.y + 12)];
  step(s);
  assert.equal(s.mode, 'won'); assert.equal(s.boss.phase, 'defeated'); assert.equal(s.shots.length, 0);
  const time = s.elapsed; step(s, {}, 60); assert.equal(s.elapsed, time);
  s.deaths = 5; s.kills = 20;
  startGame(s);
  assert.equal(s.mode, 'playing'); assert.equal(s.elapsed, 0); assert.equal(s.deaths, 0); assert.equal(s.kills, 0);
  assert.equal(s.checkpoint, 0); assert.equal(s.player.x, 64); assert.equal(s.boss.active, false);
});

test('fatal contact cannot be overwritten by a simultaneous boss hit or health pickup', () => {
  const s = game(); s.player.x = s.level.arena.x + 30; step(s);
  s.player.hp = 1; s.player.invulnerable = 0;
  s.boss.x = s.player.x; s.boss.hp = 1;
  s.shots = [shot(s.boss.x, s.boss.y + 12)];
  s.pickups = [{ x: s.player.x, y: s.player.y, w: 20, h: 25, kind: 'health', life: 2 }];
  step(s);
  assert.equal(s.mode, 'dead'); assert.equal(s.player.hp, 0); assert.ok(s.boss.hp > 0);
});

test('the complete highway and factory are traversable using ordinary jumps', () => {
  const s = game(); let wasJump = false;
  for (let i = 0; i < 9000 && s.player.x < s.level.arena.x + 30; i++) {
    const p = s.player; const bottom = p.y + p.h;
    const support = s.level.platforms.find(t => Math.abs(t.y - bottom) < 2 && p.x < t.x + t.w && p.x + p.w > t.x);
    const ahead = s.level.platforms.some(t => t.x >= p.x + p.w - 1 && t.x < p.x + p.w + 36 && t.y < bottom - 1 && t.y >= bottom - 60);
    const edge = support && support.x + support.w - (p.x + p.w) < 19;
    const jump: boolean = p.grounded ? Boolean(ahead || edge) : wasJump;
    step(s, { right: true, jump, jumpPressed: jump && !wasJump, jumpReleased: !jump && wasJump });
    wasJump = p.grounded ? false : jump;
    assert.equal(s.mode, 'playing', `Route failed at x=${Math.round(p.x)}, y=${Math.round(p.y)}`);
  }
  assert.ok(s.player.x >= s.level.arena.x + 30, `Route stalled at x=${Math.round(s.player.x)}, y=${Math.round(s.player.y)}`);
  assert.equal(s.checkpoint, 2);
});
