import { describe, expect, it } from "vitest";
import { PROTOCOL_VERSION } from "../types/net.js";
import { validateClientMessage, validateHostMessage, validateSignalMessage } from "./net.js";

/**
 * These validators sit on the boundary between the host and an untrusted peer.
 * They are expected to *sanitise*, not merely accept or reject, so the tests
 * assert on the values that come out rather than only on the ok/fail verdict.
 */

describe("validateClientMessage", () => {
  it("accepts well-formed input", () => {
    const result = validateClientMessage({
      t: "input",
      tick: 12,
      move: { x: 0.5, z: -1 },
      run: true,
      yaw: 1.57,
    });

    expect(result.ok).toBe(true);
    expect(result.ok && result.value).toMatchObject({
      t: "input",
      tick: 12,
      move: { x: 0.5, z: -1 },
      run: true,
    });
  });

  it("clamps an out-of-range movement vector instead of rejecting it", () => {
    const result = validateClientMessage({
      t: "input",
      tick: 1,
      move: { x: 1e9, z: -1e9 },
      run: true,
      yaw: 0,
    });

    expect(result.ok).toBe(true);
    expect(result.ok && result.value).toMatchObject({ move: { x: 1, z: -1 } });
  });

  it("rejects NaN and Infinity, which survive JSON round trips", () => {
    expect(
      validateClientMessage({
        t: "input",
        tick: 1,
        move: { x: Number.NaN, z: 0 },
        run: false,
        yaw: 0,
      }).ok,
    ).toBe(false);

    expect(
      validateClientMessage({
        t: "input",
        tick: 1,
        move: { x: 0, z: 0 },
        run: false,
        yaw: Number.POSITIVE_INFINITY,
      }).ok,
    ).toBe(false);
  });

  it("coerces a non-boolean run flag rather than trusting it", () => {
    const result = validateClientMessage({
      t: "input",
      tick: 1,
      move: { x: 0, z: 0 },
      run: "yes",
      yaw: 0,
    });
    expect(result.ok && result.value).toMatchObject({ run: false });
  });

  it("floors a fractional tick", () => {
    const result = validateClientMessage({
      t: "input",
      tick: 7.9,
      move: { x: 0, z: 0 },
      run: false,
      yaw: 0,
    });
    expect(result.ok && result.value).toMatchObject({ tick: 7 });
  });

  it("caps chat length so one frame cannot exhaust host memory", () => {
    const result = validateClientMessage({ t: "chat", text: "x".repeat(10_000) });
    expect(result.ok).toBe(true);
    expect(result.ok && (result.value as { text: string }).text.length).toBe(280);
  });

  it("rejects an over-long interactable id", () => {
    expect(validateClientMessage({ t: "interact", interactableId: "x".repeat(500) }).ok).toBe(false);
  });

  it("rejects unknown message types", () => {
    expect(validateClientMessage({ t: "shutdown" }).ok).toBe(false);
    expect(validateClientMessage(null).ok).toBe(false);
    expect(validateClientMessage([]).ok).toBe(false);
  });
});

describe("validateSignalMessage", () => {
  it("overwrites a claimed sender id", () => {
    const result = validateSignalMessage({
      t: "signal",
      from: 99,
      to: 1,
      payload: { sdp: { type: "offer", sdp: "v=0" } },
    });

    // The relay stamps the true sender; a claimed `from` must not survive.
    expect(result.ok && result.value).toMatchObject({ from: -1, to: 1 });
  });

  it("rejects a protocol version mismatch", () => {
    const result = validateSignalMessage({
      t: "hello",
      token: "abc",
      lobbyCode: "ABCDE",
      protocol: PROTOCOL_VERSION + 1,
    });

    expect(result.ok).toBe(false);
    expect(!result.ok && result.errors[0]).toContain("protocol mismatch");
  });

  it("accepts a valid hello", () => {
    const result = validateSignalMessage({
      t: "hello",
      token: "abc",
      lobbyCode: "ABCDE",
      protocol: PROTOCOL_VERSION,
    });
    expect(result.ok).toBe(true);
  });
});

describe("validateHostMessage", () => {
  it("rejects a snapshot carrying more players than the lobby allows", () => {
    const players = Array.from({ length: 9 }, (_, i) => ({
      peerId: i,
      p: { x: 0, y: 0, z: 0 },
      yaw: 0,
      spd: 0,
      grounded: true,
    }));

    expect(validateHostMessage({ t: "snapshot", tick: 1, ack: {}, players }).ok).toBe(false);
  });

  it("rejects a snapshot with a non-finite position", () => {
    const result = validateHostMessage({
      t: "snapshot",
      tick: 1,
      ack: {},
      players: [
        { peerId: 1, p: { x: Number.NaN, y: 0, z: 0 }, yaw: 0, spd: 0, grounded: true },
      ],
    });
    expect(result.ok).toBe(false);
  });

  it("accepts a well-formed snapshot", () => {
    const result = validateHostMessage({
      t: "snapshot",
      tick: 4,
      ack: { 1: 3 },
      players: [{ peerId: 1, p: { x: 1, y: 0, z: 2 }, yaw: 0.4, spd: 1.2, grounded: true }],
    });
    expect(result.ok).toBe(true);
  });

  it("rejects malformed flag payloads", () => {
    expect(validateHostMessage({ t: "flags", set: "everything", unset: [] }).ok).toBe(false);
    expect(validateHostMessage({ t: "flags", set: [1, 2], unset: [] }).ok).toBe(false);
  });
});
