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 { decide } from '../robot/strategy';
import type { Move } from '../dict/validate';

// A full-loop smoke: two robots play a whole local vs_ai game to completion over the committed
// sample dictionary, exercising deal / play / refill / exchange / pass / end detection / winner.
// (The rich full-dictionary robot behaviour is pinned elsewhere by the generator + strategy parity
// suites; this test proves the engine drives to a valid finish.)
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;
}

// robotTurn plays the seat to move: it picks the robot's action from the ranked legal plays and
// dispatches it. The sample dict has a one-letter word ("a") the generator emits but the validator
// rejects (len < 2) — real dictionaries have none, so filtering to main length >= 2 is a no-op in
// production; an exchange the bag is too small to satisfy falls back to a pass.
function robotTurn(game: LocalGame, seed: bigint): void {
  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') {
    game.play(dec.move.dir, dec.move.tiles);
  } else if (dec.kind === 'exchange' && game.bagLength >= RACK_SIZE) {
    game.exchange(dec.exchange);
  } else {
    game.pass();
  }
}

describe('offline engine full-game smoke', () => {
  it('plays a whole 2-player vs_ai game to completion', () => {
    const game = new LocalGame({
      variant: 'scrabble_en',
      version: 'sample',
      seed: 123456789n,
      players: 2,
      dawg,
      multipleWords: true,
    });

    let turns = 0;
    while (!game.isOver && turns < 2000) {
      robotTurn(game, game.seed);
      turns++;
    }

    expect(game.isOver).toBe(true);
    expect(turns).toBeLessThan(2000);
    expect(['out_of_tiles', 'scoreless', 'resign']).toContain(game.endReason);

    const w = game.winnerIndex;
    expect(w === -1 || (w >= 0 && w < game.playerCount)).toBe(true);
    for (let i = 0; i < game.playerCount; i++) expect(Number.isInteger(game.scoreOf(i))).toBe(true);
    // The move log recorded every turn.
    expect(game.history.length).toBe(turns);
  });

  it('is reproducible from the seed', () => {
    const play = (): { reason: string; scores: number[]; turns: number } => {
      const g = new LocalGame({ variant: 'scrabble_en', version: 'sample', seed: 42n, players: 2, dawg, multipleWords: true });
      let t = 0;
      while (!g.isOver && t < 2000) {
        robotTurn(g, g.seed);
        t++;
      }
      return { reason: g.endReason, scores: [g.scoreOf(0), g.scoreOf(1)], turns: t };
    };
    expect(play()).toEqual(play());
  });
});
