import { describe, it, expect } from 'vitest';
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { Dawg } from './dawg';
import { generateMoves, GenRack, type GenBoard, type Mode } from './generate';
import type { Ruleset } from './validate';

// Full-dictionary conformance for the ported move generator: for positions over the real
// shipped dictionaries (deep graphs and full 26/33-letter alphabets the tiny committed
// samples cannot reach) it must return exactly the ranked list the Go solver does. The
// golden vectors come from `go run ./backend/cmd/movegen -dawg-dir <dawg> -out <dir>` and
// the CI conformance job wires the two directories in; the suite skips when they are unset.
const dawgDir = process.env.DICT_DAWG_DIR;
const goldDir = process.env.DICT_MOVEGEN_DIR;
const ready = !!dawgDir && !!goldDir && existsSync(dawgDir) && existsSync(goldDir);

// variant -> the release dawg file name (matches dawg.parity.test.ts / the movegen tool).
const variants = [
  { variant: 'scrabble_en', dawg: 'en_sowpods' },
  { variant: 'scrabble_ru', dawg: 'ru_scrabble' },
  { variant: 'erudit_ru', dawg: 'ru_erudit' },
];

interface Tile {
  row: number;
  col: number;
  letter: number;
  blank: boolean;
}
interface GenMove {
  dir: number;
  tiles: Tile[];
  score: number;
}
interface Fixture {
  ruleset: {
    size: number;
    cols: number;
    center: number;
    rackSize: number;
    bingo: number;
    values: number[];
    letterMult: number[][];
    wordMult: number[][];
  };
  cases: {
    name: string;
    placed: Tile[] | null;
    rack: { letters: number[]; blanks: number };
    mode: number;
    ignoreCrossWords: boolean;
    moves: GenMove[];
  }[];
}

function buildBoard(placed: Tile[], cols: number): GenBoard {
  const grid: ({ letter: number; blank: boolean } | null)[] = new Array(cols * cols).fill(null);
  for (const t of placed) grid[t.row * cols + t.col] = { letter: t.letter, blank: t.blank };
  const inBounds = (r: number, c: number): boolean => r >= 0 && r < cols && c >= 0 && c < cols;
  return {
    rows: cols,
    cols,
    inBounds,
    filled: (r, c) => inBounds(r, c) && grid[r * cols + c] !== null,
    cellAt: (r, c) => grid[r * cols + c]!,
    isEmpty: () => placed.length === 0,
  };
}

function rulesetFor(fx: Fixture, ignoreCrossWords: boolean): Ruleset {
  const r = fx.ruleset;
  return {
    cols: r.cols,
    center: r.center,
    rackSize: r.rackSize,
    bingo: r.bingo,
    values: r.values,
    letterMult: (row, col) => r.letterMult[row][col],
    wordMult: (row, col) => r.wordMult[row][col],
    ignoreCrossWords,
  };
}

function sig(m: GenMove): string {
  const ts = m.tiles.slice().sort((a, b) => (a.row !== b.row ? a.row - b.row : a.col - b.col));
  return `${m.dir}#${m.score}#` + ts.map((t) => `${t.row},${t.col},${t.letter}${t.blank ? '*' : ''}`).join(';');
}

describe.skipIf(!ready)('move generator parity vs Go on the real dictionaries', () => {
  for (const v of variants) {
    describe(v.variant, () => {
      // Guard the collection-time reads: the block is skipped when the dirs are unset, but
      // the describe callback still runs to register tests, so touch the filesystem only
      // when ready (otherwise join(undefined, …) would throw during a normal unit run).
      if (!ready) return;
      const fx = JSON.parse(readFileSync(join(goldDir!, `${v.variant}.movegen.json`), 'utf8')) as Fixture;
      const dawg = new Dawg(new Uint8Array(readFileSync(join(dawgDir!, `${v.dawg}.dawg`))));
      for (const c of fx.cases) {
        it(
          c.name,
          () => {
            const board = buildBoard(c.placed ?? [], fx.ruleset.cols);
            const rack = GenRack.from(fx.ruleset.size, c.rack.letters, c.rack.blanks);
            const rs = rulesetFor(fx, c.ignoreCrossWords);
            const got = generateMoves(dawg, board, rack, rs, c.mode as Mode);
            expect(got.map(sig)).toEqual(c.moves.map(sig));
          },
          60_000,
        );
      }
    });
  }
});
