import { describe, it, expect } from 'vitest';
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { Dawg } from './dawg';
import { validatePlay, type Board, type Ruleset, type Placement, type Cell, type Direction } from './validate';
import { playDirection } from './direction';
import { premiumGrid, centre, BINGO, RACK_SIZE, BOARD_SIZE } from '../premiums';
import type { Variant } from '../model';

// Conformance gate for the ported validator: for every fixture produced by
// `go run ./backend/cmd/validategen`, validatePlay must agree with the Go engine
// on legality, and — when legal — on the score, the bonus and the exact words
// formed (main + cross), across all variants and both cross-word rules. Point it
// at the fixtures + dictionaries with:
//   DICT_DAWG_DIR=<scrabble-solver>/dawg DICT_VALID_DIR=<validategen out>
// It skips when those are unset (kept out of the default unit run until a
// committed sample fixture lands).
const dawgDir = process.env.DICT_DAWG_DIR;
const validDir = process.env.DICT_VALID_DIR;
const ready = !!dawgDir && !!validDir && existsSync(dawgDir) && existsSync(validDir);

const dawgFile: Record<string, string> = {
  scrabble_en: 'en_sowpods.dawg',
  scrabble_ru: 'ru_scrabble.dawg',
  erudit_ru: 'ru_erudit.dawg',
};

interface FxCell {
  R: number;
  C: number;
  Letter: number;
  Blank: boolean;
}
interface FxWord {
  Letters: number[];
  Score: number;
}
interface Fx {
  board: number;
  dir: number;
  ignoreCrossWords: boolean;
  tiles: FxCell[];
  legal: boolean;
  score: number;
  bonus: number;
  main?: FxWord;
  cross?: FxWord[];
}
interface VF {
  rows: number;
  cols: number;
  center: number;
  rackSize: number;
  bingo: number;
  values: number[];
  premiums: number[];
  boards: (FxCell[] | null)[];
  fixtures: Fx[];
}

// Mirror rules.Premium.LetterMult / WordMult over the premium codes
// (0 none, 1 DL, 2 TL, 3 DW, 4 TW) emitted by validategen.
const letterMultOf = (code: number): number => (code === 1 ? 2 : code === 2 ? 3 : 1);
const wordMultOf = (code: number): number => (code === 3 ? 2 : code === 4 ? 3 : 1);

function makeBoard(rows: number, cols: number, cells: FxCell[]): Board {
  const grid: (Cell | null)[] = new Array(rows * cols).fill(null);
  for (const cl of cells) grid[cl.R * cols + cl.C] = { letter: cl.Letter, blank: cl.Blank };
  const inBounds = (r: number, c: number) => r >= 0 && r < rows && c >= 0 && c < cols;
  return {
    inBounds,
    filled: (r, c) => inBounds(r, c) && grid[r * cols + c] !== null,
    cellAt: (r, c) => grid[r * cols + c] as Cell,
    isEmpty: () => cells.length === 0,
  };
}

function arrEq(a: number[], b: number[]): boolean {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
  return true;
}

describe.skipIf(!ready)('validator parity vs Go engine', () => {
  for (const variant of Object.keys(dawgFile)) {
    it(
      variant,
      () => {
        const vf: VF = JSON.parse(readFileSync(join(validDir!, `${variant}.fixtures.json`), 'utf8'));
        const dawg = new Dawg(new Uint8Array(readFileSync(join(dawgDir!, dawgFile[variant]))));
        const v = variant as Variant;

        // Pin the client's static per-variant rule constants to the engine's ruleset.
        expect(RACK_SIZE, 'rack size').toBe(vf.rackSize);
        expect(BINGO[v], 'bingo').toBe(vf.bingo);
        const ctr = centre(v);
        expect(ctr.row * BOARD_SIZE + ctr.col, 'centre').toBe(vf.center);
        const clientPrem = premiumGrid(v);
        const premCode = (p: string): number => (p === 'DL' ? 1 : p === 'TL' ? 2 : p === 'DW' ? 3 : p === 'TW' ? 4 : 0);
        let premOk = true;
        for (let r = 0; r < vf.rows; r++)
          for (let c = 0; c < vf.cols; c++) if (premCode(clientPrem[r][c]) !== vf.premiums[r * vf.cols + c]) premOk = false;
        expect(premOk, 'premium grid matches engine').toBe(true);

        let legalCount = 0;
        let illegalCount = 0;
        let mismatches = 0;
        let dirMismatches = 0;
        const first: string[] = [];
        const firstDir: string[] = [];

        for (let fi = 0; fi < vf.fixtures.length; fi++) {
          const fx = vf.fixtures[fi];
          const board = makeBoard(vf.rows, vf.cols, vf.boards[fx.board] ?? []);
          const rs: Ruleset = {
            cols: vf.cols,
            center: vf.center,
            rackSize: vf.rackSize,
            bingo: vf.bingo,
            values: vf.values,
            letterMult: (r, c) => letterMultOf(vf.premiums[r * vf.cols + c]),
            wordMult: (r, c) => wordMultOf(vf.premiums[r * vf.cols + c]),
            ignoreCrossWords: fx.ignoreCrossWords,
          };
          const tiles: Placement[] = fx.tiles.map((t) => ({ row: t.R, col: t.C, letter: t.Letter, blank: t.Blank }));
          const res = validatePlay(board, rs, dawg, fx.dir as Direction, tiles);

          const gotDir = playDirection(board, rs, dawg, tiles);
          if (gotDir !== fx.dir) {
            dirMismatches++;
            if (firstDir.length < 8) firstDir.push(`#${fi} dir exp ${fx.dir} got ${gotDir}`);
          }

          if (fx.legal) legalCount++;
          else illegalCount++;

          let ok = res.legal === fx.legal;
          if (ok && fx.legal) {
            const m = res.move!;
            ok =
              m.score === fx.score &&
              m.bonus === fx.bonus &&
              arrEq(m.main.letters, fx.main!.Letters) &&
              m.cross.length === (fx.cross?.length ?? 0) &&
              m.cross.every((w, i) => arrEq(w.letters, fx.cross![i].Letters));
          }
          if (!ok) {
            mismatches++;
            if (first.length < 8) {
              first.push(`#${fi} exp legal=${fx.legal} score=${fx.score} got legal=${res.legal} score=${res.move?.score ?? '-'}`);
            }
          }
        }

        expect(legalCount, 'fixtures should include legal plays').toBeGreaterThan(0);
        expect(illegalCount, 'fixtures should include illegal plays').toBeGreaterThan(0);
        expect(mismatches, first.join(' | ')).toBe(0);
        expect(dirMismatches, firstDir.join(' | ')).toBe(0);
      },
      120_000,
    );
  }
});
