import { describe, it, expect } from 'vitest';
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { Dawg } from './dawg';
import { evaluateLocal } from './eval';
import { setAlphabet } from '../alphabet';
import type { Board as ClientBoard, BoardCell } from '../board';
import type { PlacedTile } from '../client';
import type { Variant } from '../model';

// End-to-end conformance for the local-eval ADAPTER: it drives evaluateLocal through
// the real letter-space client path (the server-sent alphabet table, a letter board,
// letter placements) and asserts it matches the Go engine's EvalResult for every
// fixture. This is the cross-test the algorithm-level validate.parity suite does not
// cover — it exercises the letter<->index mapping, the ruleset assembly and the word
// decode, not just the index-space validator. Env-gated like the other parity suites.
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[];
}
interface Fx {
  board: number;
  ignoreCrossWords: boolean;
  tiles: FxCell[];
  legal: boolean;
  score: number;
  main?: FxWord;
  cross?: FxWord[];
}
interface Alpha {
  index: number;
  letter: string;
  value: number;
}
interface VF {
  rows: number;
  cols: number;
  alphabet: Alpha[];
  boards: (FxCell[] | null)[];
  fixtures: Fx[];
}

describe.skipIf(!ready)('local-eval adapter 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;

        // Seed the alphabet cache exactly as the server would (wire entries, lower-cased
        // letters); the adapter re-encodes through it, just like production.
        setAlphabet(v, vf.alphabet.map((a) => ({ index: a.index, letter: a.letter, value: a.value })));
        const upper = vf.alphabet.map((a) => a.letter.toUpperCase());
        const lower = vf.alphabet.map((a) => a.letter);

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

        for (let fi = 0; fi < vf.fixtures.length; fi++) {
          const fx = vf.fixtures[fi];

          // Build the client's letter-space board and placements from the fixture.
          const board: ClientBoard = Array.from({ length: vf.rows }, () =>
            Array.from({ length: vf.cols }, () => null as BoardCell | null),
          );
          for (const cl of vf.boards[fx.board] ?? []) board[cl.R][cl.C] = { letter: upper[cl.Letter], blank: cl.Blank };
          const tiles: PlacedTile[] = fx.tiles.map((t) => ({ row: t.R, col: t.C, letter: upper[t.Letter], blank: t.Blank }));

          const res = evaluateLocal(dawg, v, board, tiles, !fx.ignoreCrossWords);

          if (fx.legal) legalCount++;

          let ok = res.legal === fx.legal;
          if (ok && fx.legal) {
            const wantWords = [fx.main!, ...(fx.cross ?? [])].map((w) => w.Letters.map((i) => lower[i]).join(''));
            ok = res.score === fx.score && res.words.length === wantWords.length && res.words.every((w, i) => w === wantWords[i]);
          }
          if (!ok) {
            mismatches++;
            if (first.length < 8) {
              first.push(`#${fi} exp legal=${fx.legal} score=${fx.score} got legal=${res.legal} score=${res.score} words=${JSON.stringify(res.words)}`);
            }
          }
        }

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