// @vitest-environment node
import { describe, it, expect, afterAll } from 'vitest';
import { WebSocket } from 'ws';
import type { AddressInfo } from 'node:net';
import { buildServer } from './index';
import type { ClientMessage, ServerMessage } from '@/lib/sync/messages';

const app = buildServer();
let base = '';

async function start(): Promise<void> {
  await app.listen({ port: 0, host: '127.0.0.1' });
  const addr = app.server.address() as AddressInfo;
  base = `ws://127.0.0.1:${addr.port}/ws`;
}
afterAll(async () => { await app.close(); });

function open(): Promise<WebSocket> {
  const ws = new WebSocket(base);
  return new Promise((res, rej) => { ws.on('open', () => res(ws)); ws.on('error', rej); });
}
function send(ws: WebSocket, msg: ClientMessage): void { ws.send(JSON.stringify(msg)); }
function next(ws: WebSocket, t: ServerMessage['t']): Promise<ServerMessage> {
  return new Promise((resolve) => {
    const onMsg = (raw: Buffer | ArrayBuffer | Buffer[]) => {
      const m = JSON.parse(raw.toString()) as ServerMessage;
      if (m.t === t) { ws.off('message', onMsg); resolve(m); }
    };
    ws.on('message', onMsg);
  });
}
const snap = { campaignName: 'Live', calendarDay: null, party: [], encounter: null, map: null, mapImageId: null, quests: [] };

describe('realtime server (integration)', () => {
  it('host → join → snapshot flow; players cannot push state', async () => {
    await start();
    const gm = await open();
    send(gm, { t: 'host', campaignId: 'c' });
    const hosted = await next(gm, 'hosted') as Extract<ServerMessage, { t: 'hosted' }>;
    expect(hosted.joinCode).toBeTruthy();

    const player = await open();
    send(player, { t: 'join', joinCode: hosted.joinCode });
    await next(player, 'joined');

    send(gm, { t: 'state', gmSecret: hosted.gmSecret, snapshot: snap });
    const got = await next(player, 'snapshot') as Extract<ServerMessage, { t: 'snapshot' }>;
    expect(got.snapshot.campaignName).toBe('Live');

    // A player pushing state is rejected.
    send(player, { t: 'state', gmSecret: 'forged', snapshot: snap });
    const err = await next(player, 'error') as Extract<ServerMessage, { t: 'error' }>;
    expect(err.code).toBe('forbidden');

    gm.close(); player.close();
  });
});
