import { describe, expect, it } from "vitest";
import type { PlayerSnapshot } from "@psx/shared";
import { INTERPOLATION_DELAY, SnapshotBuffer } from "./SnapshotBuffer.js";

/**
 * Interpolation is where a networked game visibly succeeds or fails, and it is
 * pure arithmetic over a buffer — so it is worth testing directly rather than
 * discovering the bugs by watching an avatar stutter.
 */

const snap = (peerId: number, x: number, yaw = 0, spd = 0): PlayerSnapshot => ({
  peerId,
  p: { x, y: 0, z: 0 },
  yaw,
  spd,
  grounded: true,
});

describe("SnapshotBuffer", () => {
  it("returns nothing while empty", () => {
    expect(new SnapshotBuffer().sample(10)).toBeNull();
  });

  it("interpolates halfway between two snapshots", () => {
    const buffer = new SnapshotBuffer();
    buffer.push(snap(1, 0), 1);
    buffer.push(snap(1, 10), 2);

    // Render time sits exactly between the two samples.
    const pose = buffer.sample(1.5 + INTERPOLATION_DELAY);

    expect(pose).not.toBeNull();
    expect(pose!.position.x).toBeCloseTo(5, 5);
    expect(pose!.fresh).toBe(true);
  });

  it("takes the short way around when yaw wraps past PI", () => {
    const buffer = new SnapshotBuffer();
    // 170° → -170° is a 20° step forward, not 340° backward.
    buffer.push(snap(1, 0, (170 * Math.PI) / 180), 1);
    buffer.push(snap(1, 0, (-170 * Math.PI) / 180), 2);

    const pose = buffer.sample(1.5 + INTERPOLATION_DELAY)!;
    const degrees = (pose.yaw * 180) / Math.PI;

    // Halfway is 180° (equivalently -180°), never near 0°.
    expect(Math.abs(Math.abs(degrees) - 180)).toBeLessThan(1);
  });

  it("holds the oldest pose before the buffer has filled", () => {
    const buffer = new SnapshotBuffer();
    buffer.push(snap(1, 42), 100);

    // Render time is well before anything we hold.
    const pose = buffer.sample(100)!;
    expect(pose.position.x).toBe(42);
  });

  it("marks a pose stale once the sender stops updating", () => {
    const buffer = new SnapshotBuffer();
    buffer.push(snap(1, 7), 1);

    const soon = buffer.sample(1 + INTERPOLATION_DELAY + 0.05)!;
    expect(soon.fresh).toBe(true);

    // Half a second past the newest snapshot: the peer has clearly stalled.
    const late = buffer.sample(1 + INTERPOLATION_DELAY + 0.5)!;
    expect(late.fresh).toBe(false);
    expect(late.position.x).toBe(7);
  });

  it("ignores snapshots that arrive out of order", () => {
    const buffer = new SnapshotBuffer();
    buffer.push(snap(1, 0), 1);
    buffer.push(snap(1, 10), 2);
    // Late arrival from before the newest; interpolating through it would
    // rewind the avatar.
    buffer.push(snap(1, 999), 1.5);

    expect(buffer.size).toBe(2);
    const pose = buffer.sample(1.5 + INTERPOLATION_DELAY)!;
    expect(pose.position.x).toBeCloseTo(5, 5);
  });

  it("bounds memory under a long session", () => {
    const buffer = new SnapshotBuffer();
    for (let i = 0; i < 5000; i++) buffer.push(snap(1, i), i);
    expect(buffer.size).toBeLessThanOrEqual(32);
  });
});
