import { describe, expect, it } from 'vitest';
import {
  RoomSnapshotSchema,
  SERVER_EVENTS,
  ServerEnvelopeSchema,
  ServerMessageSchema,
  envelope,
  isServerEventName,
  parseServerMessage,
  type ServerEnvelope,
  type ServerEventName,
} from '../src';
import { IDS, NOW, hostState, privateState, publicOpen, snapshot, standing } from './fixtures';

const meta = { seq: 7, serverNow: NOW };

/** One valid envelope per server event. */
const SAMPLES: { [E in ServerEventName]: ServerEnvelope<E> } = {
  'room.state': envelope('room.state', meta, { snapshot: snapshot() }),
  'round.start': envelope('round.start', meta, { public: publicOpen() }),
  'question.pending': envelope('question.pending', meta, { public: publicOpen() }),
  'question.open': envelope('question.open', meta, { public: publicOpen() }),
  'question.lock': envelope('question.lock', meta, { public: publicOpen() }),
  'question.reveal': envelope('question.reveal', meta, { public: publicOpen() }),
  'answer.result': envelope('answer.result', meta, { me: privateState() }),
  'board.updated': envelope('board.updated', meta, { me: privateState() }),
  'score.updated': envelope('score.updated', meta, { me: privateState() }),
  'leaderboard.updated': envelope('leaderboard.updated', meta, { public: publicOpen() }),
  'round.complete': envelope('round.complete', meta, {
    public: publicOpen(),
    data: {
      summary: {
        id: IDS.round,
        index: 0,
        kind: 'RUSH',
        label: 'The Rush',
        completedAt: NOW,
        podium: [{ playerId: IDS.alice, nickname: 'Alice', points: 900 }],
      },
      standings: [standing(1, IDS.alice, 'Alice')],
    },
  }),
  'event.complete': envelope('event.complete', meta, { public: null, data: { standings: [standing(1, IDS.alice, 'Alice')] } }),
  'event.state': envelope('event.state', meta, {
    data: { activeSessionId: IDS.session, activeEventKind: 'CHAMPIONSHIP', activeEventName: 'Thursday Championship' },
  }),
  'host.state': envelope('host.state', meta, { host: hostState() }),
  'session.token': envelope('session.token', meta, {
    data: { token: 'eyJ.header.sig', playerSessionId: IDS.alice, gameSessionId: IDS.session, expiresAt: NOW + 21_600_000 },
  }),
  'player.kicked': envelope('player.kicked', meta, { data: { playerSessionId: IDS.alice } }),
  error: envelope('error', meta, { data: { code: 'RATE_LIMITED', message: 'Slow down' } }),
};

describe('SERVER_EVENTS', () => {
  it('lists the spec events plus pending/reveal/state/token/kick/error', () => {
    expect(SERVER_EVENTS).toEqual([
      'room.state',
      'round.start',
      'question.pending',
      'question.open',
      'question.lock',
      'question.reveal',
      'answer.result',
      'board.updated',
      'score.updated',
      'leaderboard.updated',
      'round.complete',
      'event.complete',
      'event.state',
      'host.state',
      'session.token',
      'player.kicked',
      'error',
    ]);
    expect(isServerEventName('question.open')).toBe(true);
    expect(isServerEventName('question.answer')).toBe(false);
  });

  it('has exactly one message variant per event', () => {
    expect(ServerMessageSchema.options.map((o) => o.shape.type.value).sort()).toEqual([...SERVER_EVENTS].sort());
  });
});

describe('envelope()', () => {
  it('builds frames that pass both the generic and the exact schema', () => {
    for (const name of SERVER_EVENTS) {
      const msg = SAMPLES[name];
      expect(msg.type).toBe(name);
      expect(msg.seq).toBe(7);
      expect(ServerEnvelopeSchema.safeParse(msg).error?.issues, name).toBeUndefined();
      expect(parseServerMessage(msg).error?.issues, name).toBeUndefined();
    }
  });

  it('survives a JSON round trip (what Socket.IO puts on the wire)', () => {
    for (const name of SERVER_EVENTS) {
      const wire: unknown = JSON.parse(JSON.stringify(SAMPLES[name]));
      expect(parseServerMessage(wire).data, name).toEqual(SAMPLES[name]);
    }
  });
});

describe('ServerEnvelopeSchema (generic frame)', () => {
  it('parses a minimal envelope and leaves data unchecked', () => {
    const parsed = ServerEnvelopeSchema.parse({ type: 'question.open', seq: 0, serverNow: NOW, data: { anything: 1 } });
    expect(parsed).toEqual({ type: 'question.open', seq: 0, serverNow: NOW, data: { anything: 1 } });
  });

  it('rejects bad frames', () => {
    const base = { type: 'question.open', seq: 1, serverNow: NOW };
    expect(ServerEnvelopeSchema.safeParse({ ...base, type: 'question.answer' }).success).toBe(false);
    expect(ServerEnvelopeSchema.safeParse({ ...base, seq: -1 }).success).toBe(false);
    expect(ServerEnvelopeSchema.safeParse({ ...base, seq: 1.5 }).success).toBe(false);
    expect(ServerEnvelopeSchema.safeParse({ type: 'question.open', seq: 1 }).success).toBe(false);
    expect(ServerEnvelopeSchema.safeParse({ ...base, extra: true }).success).toBe(false);
  });

  it('applies the strict view schemas to embedded state', () => {
    const leaky = structuredClone(publicOpen()) as Record<string, any>;
    leaky.question.correctOptionId = 'c';
    expect(ServerEnvelopeSchema.safeParse({ type: 'question.open', seq: 1, serverNow: NOW, public: leaky }).success).toBe(false);
  });
});

describe('ServerMessageSchema (exact per event)', () => {
  it('requires a snapshot on room.state and host on host.state', () => {
    expect(parseServerMessage({ type: 'room.state', ...meta }).success).toBe(false);
    expect(parseServerMessage({ type: 'host.state', ...meta, public: publicOpen() }).success).toBe(false);
  });

  it('validates event data shapes', () => {
    expect(parseServerMessage({ type: 'session.token', ...meta }).success).toBe(false);
    expect(parseServerMessage({ type: 'session.token', ...meta, data: { token: 't' } }).success).toBe(false);
    expect(parseServerMessage({ type: 'error', ...meta, data: { code: 'NOPE', message: 'x' } }).success).toBe(false);
    expect(parseServerMessage({ type: 'error', ...meta, data: { code: 'TOO_LATE', message: 'Too late' } }).success).toBe(true);
    expect(parseServerMessage({ type: 'player.kicked', ...meta, data: { playerSessionId: 'p1' } }).success).toBe(false);
  });

  it('rejects data on events that define none', () => {
    expect(parseServerMessage({ type: 'question.open', ...meta, data: { correctOptionId: 'c' } }).success).toBe(false);
  });

  it('accepts room.state with an idle-room snapshot', () => {
    const idle = { ...snapshot(), activeSessionId: null, activeEventKind: null, activeEventName: null, activeEventScheduledAt: null, activeSessionStatus: null, public: null, me: null, mySessionId: null, mySessionActive: false };
    expect(RoomSnapshotSchema.safeParse(idle).success).toBe(true);
    expect(parseServerMessage({ type: 'room.state', ...meta, snapshot: idle }).success).toBe(true);
  });
});
