import { describe, it, expect } from 'vitest';
import { wordKey, playedWordKeys, noRepeatScore } from './norepeat';
import type { Direction, Move, Word } from './validate';
import { Horizontal, Vertical } from './validate';

// word builds a scored Word; only its letters and score matter to the rule.
function word(letters: number[], score: number, dir: Direction = Horizontal): Word {
  return { row: 0, col: 0, dir, letters, blanks: letters.map(() => false), score };
}

// move builds a scored play from a main word and its cross-words, with the total the engine
// would have computed (main + cross, no bingo).
function move(main: Word, cross: Word[] = []): Move {
  return {
    dir: Horizontal,
    tiles: [],
    main,
    cross,
    bonus: 0,
    score: main.score + cross.reduce((sum, w) => sum + w.score, 0),
  };
}

describe('no-repeat-words rule', () => {
  it('keys a word by its letters, so a blank spells the same word as a real tile', () => {
    // The letters are alphabet indices and the blank flag rides separately, so it never enters
    // the key — matching the server, which compares the words decoded.
    const withBlank: Word = { ...word([2, 14, 19], 5), blanks: [false, true, false] };
    expect(wordKey(withBlank.letters)).toBe(wordKey([2, 14, 19]));
  });

  it('builds the played set from the journal words, ignoring empty ones', () => {
    const played = playedWordKeys([[2, 14, 19], [], [3, 4]]);
    expect(played.has(wordKey([2, 14, 19]))).toBe(true);
    expect(played.has(wordKey([3, 4]))).toBe(true);
    expect(played.size).toBe(2);
  });

  it('rejects a play whose main word is already on the board', () => {
    const played = playedWordKeys([[2, 14, 19]]);
    expect(noRepeatScore(move(word([2, 14, 19], 7)), played)).toBeNull();
  });

  it('accepts a play whose main word is new, at its full score', () => {
    const played = playedWordKeys([[2, 14, 19]]);
    expect(noRepeatScore(move(word([17, 14, 19], 6)), played)).toBe(6);
  });

  it('keeps a play whose cross-word repeats, but scores that cross-word at nothing', () => {
    const played = playedWordKeys([[2, 14, 19]]);
    const m = move(word([17, 14, 19], 6), [word([2, 14, 19], 4, Vertical), word([8, 9], 3, Vertical)]);
    expect(m.score).toBe(13);
    expect(noRepeatScore(m, played)).toBe(9); // the repeated cross-word's 4 points are dropped
  });

  it('takes a caller-supplied key, so a decoded history can be compared directly', () => {
    // The online preview reads the words the server already decoded, so it keys on the decoded
    // form rather than on alphabet indices.
    const letters = ['', 'a', 'b', 'c'];
    const decode = (l: readonly number[]) => l.map((i) => letters[i]).join('');
    const played = new Set(['abc']);
    expect(noRepeatScore(move(word([1, 2, 3], 7)), played, decode)).toBeNull();
    expect(noRepeatScore(move(word([3, 2, 1], 7)), played, decode)).toBe(7);
  });
});
