import { describe, expect, it } from 'vitest';
import {
  PLAYER_ACCEL, PLAYER_FRICTION, PLAYER_MAX_SPEED, PLAYER_RADIUS,
  freshPlayer, stepPlayer,
} from '../../src/scenes/floor/player';
import type { PlayerInput, PlayerState } from '../../src/scenes/floor/player';
import { TILE, VENUE, isWalkableWorld, tileToWorld } from '../../src/scenes/floor/venueMap';

const NONE: PlayerInput = { up: false, down: false, left: false, right: false };
const input = (over: Partial<PlayerInput>): PlayerInput => ({ ...NONE, ...over });

// Open dance floor, and a spot one tile north of the south border wall.
const OPEN = tileToWorld(38, 23);
const NEAR_SOUTH_WALL = tileToWorld(40, 43);
const SOUTH_WALL_Y = 44 * TILE;

function run(p: PlayerState, i: PlayerInput, steps: number, ms = 16): PlayerState {
  let s = p;
  for (let n = 0; n < steps; n++) s = stepPlayer(s, i, ms, VENUE);
  return s;
}

const speedOf = (s: PlayerState): number => Math.hypot(s.vx, s.vy);

/** The collision test the mover itself uses. */
function standing(s: PlayerState): boolean {
  return isWalkableWorld(VENUE, s.x + PLAYER_RADIUS, s.y)
    && isWalkableWorld(VENUE, s.x - PLAYER_RADIUS, s.y)
    && isWalkableWorld(VENUE, s.x, s.y + PLAYER_RADIUS)
    && isWalkableWorld(VENUE, s.x, s.y - PLAYER_RADIUS);
}

describe('freshPlayer', () => {
  it('starts at rest where it was put', () => {
    const p = freshPlayer(100, 200);
    expect(p).toEqual({ x: 100, y: 200, vx: 0, vy: 0, facing: 0 });
  });

  it('exposes a heavy tuning — accel far outstrips top speed', () => {
    expect(PLAYER_ACCEL).toBeGreaterThan(PLAYER_MAX_SPEED);
    expect(PLAYER_FRICTION).toBeGreaterThan(0);
  });
});

describe('acceleration', () => {
  it('accelerates from rest', () => {
    const p = stepPlayer(freshPlayer(OPEN.x, OPEN.y), input({ right: true }), 16, VENUE);
    expect(p.vx).toBeGreaterThan(0);
    expect(p.x).toBeGreaterThan(OPEN.x);
    expect(p.vy).toBe(0);
  });

  it('clamps to PLAYER_MAX_SPEED however long you hold it', () => {
    const p = run(freshPlayer(OPEN.x, OPEN.y), input({ right: true }), 40);
    expect(speedOf(p)).toBeCloseTo(PLAYER_MAX_SPEED, 6);
    expect(speedOf(p)).toBeLessThanOrEqual(PLAYER_MAX_SPEED + 1e-9);
  });

  it('caps diagonals at the same top speed, not 1.41x', () => {
    const p = run(freshPlayer(OPEN.x, OPEN.y), input({ right: true, down: true }), 40);
    expect(speedOf(p)).toBeLessThanOrEqual(PLAYER_MAX_SPEED + 1e-9);
    expect(p.vx).toBeCloseTo(p.vy, 6);
  });
});

describe('friction', () => {
  it('decelerates with no input, without reversing', () => {
    const moving = run(freshPlayer(OPEN.x, OPEN.y), input({ right: true }), 40);
    const a = run(moving, NONE, 5);
    const b = run(a, NONE, 5);
    expect(speedOf(a)).toBeLessThan(speedOf(moving));
    expect(speedOf(b)).toBeLessThan(speedOf(a));
    expect(b.vx).toBeGreaterThan(0);
    expect(run(b, NONE, 400).vx).toBeCloseTo(0, 3);
  });
});

describe('facing', () => {
  it('follows the direction of travel', () => {
    const right = run(freshPlayer(OPEN.x, OPEN.y), input({ right: true }), 20);
    expect(right.facing).toBeCloseTo(0, 4);
    const up = run(freshPlayer(OPEN.x, OPEN.y), input({ up: true }), 20);
    expect(up.facing).toBeCloseTo(-Math.PI / 2, 4);
  });

  it('persists when input stops and the body coasts to a halt', () => {
    const up = run(freshPlayer(OPEN.x, OPEN.y), input({ up: true }), 20);
    const stopped = run(up, NONE, 600);
    expect(speedOf(stopped)).toBeLessThan(1);
    expect(stopped.facing).toBeCloseTo(up.facing, 6);
  });
});

describe('collision', () => {
  it('cannot walk through a wall', () => {
    const p = run(freshPlayer(NEAR_SOUTH_WALL.x, NEAR_SOUTH_WALL.y), input({ down: true }), 200);
    expect(standing(p)).toBe(true);
    expect(p.y + PLAYER_RADIUS).toBeLessThan(SOUTH_WALL_Y);
    expect(p.vy).toBe(0);
  });

  it('slides along a wall when moving diagonally into it', () => {
    const start = freshPlayer(NEAR_SOUTH_WALL.x, NEAR_SOUTH_WALL.y);
    const p = run(start, input({ down: true, right: true }), 120);
    expect(standing(p)).toBe(true);
    expect(p.x).toBeGreaterThan(start.x + 50);
    expect(p.y + PLAYER_RADIUS).toBeLessThan(SOUTH_WALL_Y);
  });

  it('does not tunnel on a monstrous delta', () => {
    const one = stepPlayer(freshPlayer(NEAR_SOUTH_WALL.x, NEAR_SOUTH_WALL.y), input({ down: true }), 60000, VENUE);
    expect(standing(one)).toBe(true);
    expect(one.y + PLAYER_RADIUS).toBeLessThan(SOUTH_WALL_Y);

    // ...and repeated giant deltas are still just clamped steps.
    const many = run(one, input({ down: true }), 30, 9999);
    expect(standing(many)).toBe(true);
    expect(many.y + PLAYER_RADIUS).toBeLessThan(SOUTH_WALL_Y);
  });

  it('keeps a full patrol of the open floor on walkable ground', () => {
    let p = freshPlayer(OPEN.x, OPEN.y);
    const legs: PlayerInput[] = [
      input({ right: true }), input({ down: true }), input({ left: true }),
      input({ up: true }), input({ up: true, left: true }), input({ down: true, right: true }),
    ];
    for (const leg of legs) {
      for (let n = 0; n < 300; n++) {
        p = stepPlayer(p, leg, 16, VENUE);
        expect(standing(p)).toBe(true);
      }
    }
  });
});
