import { describe, it, expect } from 'vitest';
import { clientMessageSchema, serverMessageSchema, snapshotSchema, partialCharacterDiffSchema, type Snapshot } from './messages';
import { characterSchema, type Character } from '@/lib/schemas';

const snap: Snapshot = { campaignName: 'C', calendarDay: 3, party: [], encounter: null, map: null, mapImageId: null, quests: [] };

const char: Character = characterSchema.parse({
  id: 'ch1', campaignId: 'cmp1', system: '5e', name: 'Lia',
  abilities: { str: 10, dex: 12, con: 14, int: 10, wis: 13, cha: 8 },
  hp: { current: 18, max: 24, temp: 0 }, createdAt: 't', updatedAt: 't',
});

describe('sync protocol', () => {
  it('round-trips client messages and rejects malformed', () => {
    expect(clientMessageSchema.safeParse({ t: 'join', joinCode: 'ABC123' }).success).toBe(true);
    expect(clientMessageSchema.safeParse({ t: 'state', gmSecret: 's', snapshot: snap }).success).toBe(true);
    expect(clientMessageSchema.safeParse({ t: 'nope' }).success).toBe(false);
    expect(clientMessageSchema.safeParse({ t: 'state', snapshot: snap }).success).toBe(false); // missing gmSecret
  });
  it('round-trips server messages', () => {
    expect(serverMessageSchema.safeParse({ t: 'hosted', roomId: 'r', joinCode: 'C', gmSecret: 's' }).success).toBe(true);
    expect(serverMessageSchema.safeParse({ t: 'snapshot', snapshot: snap }).success).toBe(true);
  });
  it('round-trips two-way play messages', () => {
    expect(clientMessageSchema.safeParse({ t: 'claimSeat', characterId: 'ch1' }).success).toBe(true);
    expect(clientMessageSchema.safeParse({ t: 'claimSeat', characterId: 'ch1', offlineSnapshot: char }).success).toBe(true);
    expect(clientMessageSchema.safeParse({ t: 'playerPatch', characterId: 'ch1', diff: { hp: { current: 5, max: 24, temp: 0 } } }).success).toBe(true);
    expect(clientMessageSchema.safeParse({ t: 'playerRoll', characterId: 'ch1', label: 'd20', expression: '1d20', total: 14, breakdown: '[14]' }).success).toBe(true);
    expect(clientMessageSchema.safeParse({ t: 'seatGrant', gmSecret: 's', targetPlayerId: 'p1', character: char }).success).toBe(true);
    expect(serverMessageSchema.safeParse({ t: 'seatRequest', playerId: 'p1', characterId: 'ch1' }).success).toBe(true);
    expect(serverMessageSchema.safeParse({ t: 'seatGranted', character: char }).success).toBe(true);
    expect(serverMessageSchema.safeParse({ t: 'playerPatched', characterId: 'ch1', diff: { conditions: [{ name: 'prone' }] } }).success).toBe(true);
    expect(serverMessageSchema.safeParse({ t: 'rollBroadcast', playerName: 'Lia', label: 'd20', expression: '1d20', total: 9, breakdown: '[9]' }).success).toBe(true);
  });

  it('carries concentration in a player diff (set and cleared)', () => {
    const set = partialCharacterDiffSchema.parse({ concentration: { spellId: 's1', spellName: 'Bless' } });
    expect(set.concentration).toEqual({ spellId: 's1', spellName: 'Bless' });
    const cleared = partialCharacterDiffSchema.parse({ concentration: null });
    expect(cleared.concentration).toBeNull();
  });

  it('strips identity fields from a player diff (players can never rewrite name/abilities)', () => {
    const parsed = partialCharacterDiffSchema.parse({
      hp: { current: 1, max: 24, temp: 0 },
      name: 'Hacker', abilities: { str: 99, dex: 99, con: 99, int: 99, wis: 99, cha: 99 }, level: 20,
    } as Record<string, unknown>);
    expect(parsed.hp).toEqual({ current: 1, max: 24, temp: 0 });
    expect('name' in parsed).toBe(false);
    expect('abilities' in parsed).toBe(false);
    expect('level' in parsed).toBe(false);
  });

  it('validates a full snapshot with masked enemy', () => {
    const full: Snapshot = {
      campaignName: 'X', calendarDay: null,
      party: [{ id: 'a', name: 'Hero', level: 3, ac: 15, hp: { current: 10, max: 20, temp: 0 }, conditions: [] }],
      encounter: { id: 'e', name: 'Fight', round: 1, combatants: [
        { id: 'm', name: 'Goblin', kind: 'monster', initiative: 12, isCurrent: false, status: { label: 'Bloodied', cls: 'text-warning' }, conditions: [{ name: 'frightened', value: 1 }] },
      ] },
      map: null, mapImageId: null, quests: [],
    };
    expect(snapshotSchema.safeParse(full).success).toBe(true);
  });
});
