import { afterEach, expect, test } from "bun:test";
import { createSession, removeSession } from "./store";
import { joinSession } from "./domain";
import {
  STALE_SOCKET_TIMEOUT_MS,
  handleSocketMessage,
  reapStaleSockets,
  registerSocket,
  resetWebsocketStateForTests,
  unregisterSocket,
} from "./websocket";

const OPEN = 1;
const CLOSED = 3;

type FakeSocket = {
  readyState: number;
  sent: string[];
  closes: Array<{ code?: number; reason?: string }>;
  send: (payload: string) => void;
  close: (code?: number, reason?: string) => void;
};

function createFakeSocket(): FakeSocket {
  return {
    readyState: OPEN,
    sent: [],
    closes: [],
    send(payload: string) {
      this.sent.push(payload);
    },
    close(code?: number, reason?: string) {
      this.closes.push({ code, reason });
      this.readyState = CLOSED;
    },
  };
}

afterEach(() => {
  resetWebsocketStateForTests();
});

test("overlapping sockets do not disconnect a player until the last socket closes", () => {
  const { session } = createSession("Banker");
  const player = joinSession(session, "Jules");
  const firstSocket = createFakeSocket();
  const secondSocket = createFakeSocket();

  registerSocket(firstSocket as unknown as WebSocket, session.id, player.id);
  registerSocket(secondSocket as unknown as WebSocket, session.id, player.id);

  unregisterSocket(firstSocket as unknown as WebSocket);
  expect(session.players.get(player.id)?.connected).toBe(true);
  expect(session.players.get(player.id)?.isDummy).toBe(false);

  unregisterSocket(secondSocket as unknown as WebSocket);
  expect(session.players.get(player.id)?.connected).toBe(false);
  expect(session.players.get(player.id)?.isDummy).toBe(true);

  removeSession(session.id);
});

test("stale sockets are reaped and disconnect the player", () => {
  const { session } = createSession("Banker");
  const player = joinSession(session, "Rosa");
  const socket = createFakeSocket();

  registerSocket(socket as unknown as WebSocket, session.id, player.id);
  reapStaleSockets(Date.now() + STALE_SOCKET_TIMEOUT_MS + 1);

  expect(socket.closes[0]).toEqual({ code: 4000, reason: "Connection stale" });
  expect(session.players.get(player.id)?.connected).toBe(false);
  expect(session.players.get(player.id)?.isDummy).toBe(true);

  removeSession(session.id);
});

test("invalid session registration closes the socket with a terminal code", () => {
  const socket = createFakeSocket();

  registerSocket(socket as unknown as WebSocket, "missing-session", "missing-player");

  expect(socket.closes[0]).toEqual({ code: 1008, reason: "Session not found" });
});

test("ping messages update liveness and emit a pong", () => {
  const { session } = createSession("Banker");
  const player = joinSession(session, "Nina");
  const socket = createFakeSocket();

  registerSocket(socket as unknown as WebSocket, session.id, player.id);
  socket.sent = [];

  handleSocketMessage(
    socket as unknown as WebSocket,
    JSON.stringify({ type: "ping", sessionId: session.id, playerId: player.id }),
  );

  expect(socket.sent.some((entry) => entry.includes('"type":"pong"'))).toBe(true);

  removeSession(session.id);
});
