import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { Dawg } from '../dict/dawg';
import { RACK_SIZE } from '../premiums';
import { LocalGame } from './engine';
import { wordKey } from '../dict/norepeat';
import { decide } from '../robot/strategy';
import type { Move } from '../dict/validate';

// The no-repeat-words rule driven through the whole offline engine: two robots self-play a game
// with the rule on, and no word may ever be laid twice. This covers both halves of the wiring at
// once — generateMoves must not offer a forbidden play, and play must not accept one — over a long
// run of real positions rather than a hand-built one. The pinned semantics live in
// dict/norepeat.test.ts; the same rule on the server is engine/norepeat_test.go.
const dawg = new Dawg(new Uint8Array(readFileSync(new URL('../dict/testdata/sample_en.dawg', import.meta.url))));

function bestOpponentScore(game: LocalGame, seat: number): number {
  let best = 0;
  for (let i = 0; i < game.playerCount; i++) if (i !== seat) best = Math.max(best, game.scoreOf(i));
  return best;
}

// selfPlay runs a whole game of robot turns, returning every main word laid in order. The sample
// dictionary emits a one-letter word the validator rejects, so candidates are filtered the way
// the robot's own turn does (see engine.test.ts).
function selfPlay(noRepeatWords: boolean, seed: bigint): string[] {
  const game = new LocalGame({
    variant: 'scrabble_en',
    version: 'sample',
    seed,
    players: 2,
    dawg,
    multipleWords: true,
    noRepeatWords,
  });
  const mains: string[] = [];
  for (let turn = 0; turn < 500 && !game.isOver; turn++) {
    const seat = game.currentPlayer;
    const cands: Move[] = game.generateMoves().filter((m) => m.main.letters.length >= 2);
    const dec = decide(seed, game.moveCount, cands, game.scoreOf(seat), bestOpponentScore(game, seat), game.handOf(seat), game.bagLength);
    if (dec.kind === 'play') {
      mains.push(wordKey(dec.move.main.letters));
      game.play(dec.move.dir, dec.move.tiles);
    } else if (dec.kind === 'exchange' && game.bagLength >= RACK_SIZE) {
      game.exchange(dec.exchange);
    } else {
      game.pass();
    }
  }
  return mains;
}

describe('offline engine under the no-repeat-words rule', () => {
  it('never lays the same word twice, where the same run without the rule does', () => {
    // Both halves of the wiring at once: generateMoves must not offer a forbidden play and play
    // must not accept one. Running the same seed with the rule off is the anti-vacuity guard —
    // it proves this position sequence really does offer repeats.
    const seed = 20260727n;
    const withRule = selfPlay(true, seed);
    const withoutRule = selfPlay(false, seed);

    expect(withoutRule.length).toBeGreaterThan(0);
    expect(new Set(withoutRule).size).toBeLessThan(withoutRule.length); // repeats are on offer...
    expect(new Set(withRule).size).toBe(withRule.length); // ...and the rule keeps every one out
  });
});
