import { describe, expect, it } from 'vitest';
import {
  BoardStateSchema,
  HostStateSchema,
  PrivateStateSchema,
  PublicStateSchema,
  QuestionResultSchema,
  StandingSchema,
} from '../src';
import { IDS, hostState, privateState, publicOpen, publicRevealed, standing, tiles } from './fixtures';

/** Loosely typed deep copy so tests can inject keys the types forbid. */
const mutable = <T>(value: T): Record<string, any> => structuredClone(value) as Record<string, any>;

describe('PublicStateSchema', () => {
  it('accepts an OPEN question and a REVEALED result', () => {
    expect(PublicStateSchema.parse(publicOpen())).toEqual(publicOpen());
    expect(PublicStateSchema.parse(publicRevealed())).toEqual(publicRevealed());
  });

  it('accepts an idle session (no round) and a PENDING teaser', () => {
    expect(PublicStateSchema.safeParse({ ...publicOpen(), round: null, question: null }).success).toBe(true);
    const s = mutable(publicOpen());
    s.round.phase = 'PENDING';
    s.question.prompt = null;
    s.question.options = null;
    s.question.openedAt = null;
    s.question.closesAt = null;
    expect(PublicStateSchema.safeParse(s).success).toBe(true);
  });

  it('rejects a question that carries correctOptionId', () => {
    const s = mutable(publicOpen());
    s.question.correctOptionId = 'c';
    const result = PublicStateSchema.safeParse(s);
    expect(result.success).toBe(false);
    expect(result.error?.issues[0]).toMatchObject({ code: 'unrecognized_keys', keys: ['correctOptionId'], path: ['question'] });
  });

  it('rejects answer leaks hidden in nested objects', () => {
    const option = mutable(publicOpen());
    option.question.options[2].correct = true;
    expect(PublicStateSchema.safeParse(option).success).toBe(false);

    const round = mutable(publicOpen());
    round.round.answers = {};
    expect(PublicStateSchema.safeParse(round).success).toBe(false);

    const top = mutable(publicOpen());
    top.correctOptionId = 'c';
    expect(PublicStateSchema.safeParse(top).success).toBe(false);
  });

  it('rejects a reveal before the REVEALED phase', () => {
    for (const phase of ['INTRO', 'PENDING', 'OPEN', 'LOCKED']) {
      const s = mutable(publicRevealed());
      s.round.phase = phase;
      if (phase === 'PENDING') {
        s.question.prompt = null;
        s.question.options = null;
      }
      const result = PublicStateSchema.safeParse(s);
      expect(result.success, phase).toBe(false);
      expect(result.error?.issues.some((i) => i.path[0] === 'reveal'), phase).toBe(true);
    }
  });

  it('allows the last reveal during ROUND_OVER but not for another question', () => {
    const over = mutable(publicRevealed());
    over.round.phase = 'ROUND_OVER';
    expect(PublicStateSchema.safeParse(over).success).toBe(true);

    over.reveal.questionId = IDS.round;
    expect(PublicStateSchema.safeParse(over).success).toBe(false);
  });

  it('rejects prompt/options while PENDING (no head start before open)', () => {
    const s = mutable(publicOpen());
    s.round.phase = 'PENDING';
    expect(PublicStateSchema.safeParse(s).success).toBe(false);
  });

  it('rejects a question without a round', () => {
    expect(PublicStateSchema.safeParse({ ...publicOpen(), round: null }).success).toBe(false);
  });

  it('caps public leaderboards at 10 and recent joins at 8', () => {
    const many = Array.from({ length: 11 }, (_, i) => standing(i + 1, IDS.alice, `P${i}`));
    expect(PublicStateSchema.safeParse({ ...publicOpen(), leaderboard: many }).success).toBe(false);
    expect(PublicStateSchema.safeParse({ ...publicOpen(), recentJoins: Array(9).fill('Al') }).success).toBe(false);
  });

  it('rejects missing keys and wrong enums', () => {
    const missing = mutable(publicOpen());
    delete missing.playerCount;
    expect(PublicStateSchema.safeParse(missing).success).toBe(false);

    expect(PublicStateSchema.safeParse({ ...publicOpen(), mode: 'LEAGUE' }).success).toBe(false);
  });
});

describe('PrivateStateSchema', () => {
  it('accepts a valid private view', () => {
    expect(PrivateStateSchema.parse(privateState())).toEqual(privateState());
  });

  it('accepts a revealed lastResult with the correct option (private, post-reveal)', () => {
    const s = privateState();
    s.lastResult = {
      questionId: IDS.question,
      correct: true,
      answered: true,
      correctOptionId: 'c',
      points: 100,
      speedBonus: 42,
      doubleDown: null,
      tileActivated: 4,
      newLines: [1],
      fullBoard: false,
      tokenGranted: true,
    };
    expect(PrivateStateSchema.safeParse(s).success).toBe(true);
  });

  it('rejects extra keys anywhere (strict)', () => {
    const top = mutable(privateState());
    top.wallet = { keys: [] };
    expect(PrivateStateSchema.safeParse(top).success).toBe(false);

    const powerUps = mutable(privateState());
    powerUps.powerUps.keys = ['k'];
    expect(PrivateStateSchema.safeParse(powerUps).success).toBe(false);

    const totals = mutable(privateState());
    totals.totals.xp = 1;
    expect(PrivateStateSchema.safeParse(totals).success).toBe(false);
  });

  it('rejects malformed boards', () => {
    const short = mutable(privateState());
    short.board.tiles.pop();
    expect(PrivateStateSchema.safeParse(short).success).toBe(false);

    const order = mutable(privateState());
    [order.board.tiles[0], order.board.tiles[1]] = [order.board.tiles[1], order.board.tiles[0]];
    expect(PrivateStateSchema.safeParse(order).success).toBe(false);

    const pos = mutable(privateState());
    pos.currentAnswer.wildcardPosition = 9;
    expect(PrivateStateSchema.safeParse(pos).success).toBe(false);
  });

  it('enforces the 3 power-up cap', () => {
    const s = mutable(privateState());
    s.powerUps.tokens = 4;
    expect(PrivateStateSchema.safeParse(s).success).toBe(false);
  });
});

describe('HostStateSchema', () => {
  it('accepts a valid host view', () => {
    expect(HostStateSchema.parse(hostState())).toEqual(hostState());
  });

  it('applies the public leak guards to the embedded public view', () => {
    const s = mutable(hostState());
    s.public.question.correctOptionId = 'c';
    expect(HostStateSchema.safeParse(s).success).toBe(false);
  });

  it('rejects extra roster fields', () => {
    const s = mutable(hostState());
    s.players[0].deviceHash = 'abc';
    expect(HostStateSchema.safeParse(s).success).toBe(false);
  });
});

describe('building blocks', () => {
  it('BoardStateSchema accepts a dealt board and rejects categoryLabels (private-only field)', () => {
    const board = { symmetry: 0, baseLayoutRow: 0, tiles: tiles(), lines: [0], fullBoard: false };
    expect(BoardStateSchema.safeParse(board).success).toBe(true);
    expect(BoardStateSchema.safeParse({ ...board, symmetry: 8 }).success).toBe(false);
    expect(BoardStateSchema.safeParse({ ...board, categoryLabels: {} }).success).toBe(false);
  });

  it('QuestionResultSchema allows optional challengeWinners and caps fastest at 3', () => {
    const result = publicRevealed().reveal;
    expect(QuestionResultSchema.safeParse({ ...result, challengeWinners: [IDS.team] }).success).toBe(true);
    const fastest = Array(4).fill({ playerId: IDS.alice, nickname: 'Alice', elapsedMs: 1 });
    expect(QuestionResultSchema.safeParse({ ...result, fastest }).success).toBe(false);
  });

  it('StandingSchema requires a 1-based rank', () => {
    expect(StandingSchema.safeParse(standing(1, IDS.alice, 'Alice')).success).toBe(true);
    expect(StandingSchema.safeParse(standing(0, IDS.alice, 'Alice')).success).toBe(false);
  });
});
