import { describe, it, expect, beforeAll } from 'vitest';
import { evaluateLocal } from './eval';
import { playedWordsFromHistory } from './norepeat';
import { seedMockAlphabets } from '../mock/alphabet';
import { emptyBoard, type Board } from '../board';
import type { Dict } from './validate';
import type { PlacedTile } from '../client';
import type { MoveRecord } from '../model';

// The no-repeat-words rule as the game screen sees it: through evaluateLocal, the on-device move
// preview, which decides the rule before a play ever reaches the server. The dictionary is stubbed
// to accept every word, so what these tests pin is the rule and the letter<->index adapter around
// it, not the word list (the Go/JS agreement on plain scoring is pinned by the parity suites).
const anyWord: Dict = { indexOf: () => 0 };

// place fills a board from "row,col,letter" triples.
function place(board: Board, cells: [number, number, string][]): Board {
  for (const [row, col, letter] of cells) board[row][col] = { letter, blank: false };
  return board;
}

function tile(row: number, col: number, letter: string): PlacedTile {
  return { row, col, letter, blank: false };
}

// playRecord is one history entry as the client holds it — note the UPPER-CASE words, which is how
// the codec decodes them for display (lib/codec decodeMove). Building the played set from records
// in this shape is what the game screen actually does, so these tests go through it rather than
// hand-writing the set; comparing the codec's case against the evaluator's would otherwise silently
// never match and the rule would never fire.
function playRecord(...words: string[]): MoveRecord {
  return {
    player: 0,
    action: 'play',
    dir: 'H',
    mainRow: 0,
    mainCol: 0,
    tiles: [],
    words: words.map((w) => w.toUpperCase()),
    count: 0,
    score: 0,
    total: 0,
  };
}

// passRecord is a non-play entry, which contributes no words.
function passRecord(): MoveRecord {
  return { player: 0, action: 'pass', dir: '', mainRow: 0, mainCol: 0, tiles: [], words: [], count: 0, score: 0, total: 0 };
}

beforeAll(() => seedMockAlphabets());

describe('evaluateLocal under the no-repeat-words rule', () => {
  // The position is a real one from the test contour, where the rule rejected the play on the
  // server while the composition still read as legal on the board. БРА was laid on move 4 (row 5),
  // and this play hooks the Р of ПРИНТ to spell it again downwards in column 3.
  function contourBoard(): Board {
    return place(emptyBoard(), [
      [0, 7, 'о'], [1, 7, 'в'], [2, 7, 'е'],
      [2, 5, 'д'], [2, 6, 'ж'], [2, 8, 'б'],
      [3, 7, 'н'], [3, 8, 'а'], [3, 9, 'е'], [3, 10, 'м'],
      [4, 10, 'а'],
      [5, 3, 'п'], [5, 4, 'о'], [5, 5, 'й'], [5, 6, 'к'], [5, 7, 'а'], [5, 9, 'б'], [5, 10, 'р'], [5, 11, 'а'],
      [6, 0, 'п'], [6, 3, 'у'], [6, 6, 'о'], [6, 10, 'а'],
      [7, 0, 'л'], [7, 1, 'и'], [7, 2, 'с'], [7, 3, 'т'], [7, 6, 'т'], [7, 7, 'р'], [7, 8, 'а'], [7, 9, 'в'], [7, 10, 'л'], [7, 11, 'я'],
      [8, 0, 'у'], [8, 2, 'м'], [8, 3, 'ы'], [8, 4, 'с'], [8, 5, 'и'], [8, 6, 'к'],
      [9, 0, 'г'], [9, 4, 'у'],
      [10, 4, 'ш'],
      [11, 2, 'п'], [11, 3, 'р'], [11, 4, 'и'], [11, 5, 'н'], [11, 6, 'т'],
    ]);
  }
  // Б above the standing Р and А below it spell БРА downwards.
  const bra: PlacedTile[] = [tile(10, 3, 'б'), tile(12, 3, 'а')];
  const history: MoveRecord[] = [
    playRecord('травля'), playRecord('марал'), playRecord('кот'), playRecord('наем'), playRecord('бра'),
    playRecord('пойка'), playRecord('овен'), playRecord('джеб'), playRecord('путы'), playRecord('мысик'),
    playRecord('суши'), playRecord('лист'), playRecord('плуг'), playRecord('принт'), passRecord(),
  ];
  const played = playedWordsFromHistory(history);

  it('scores the play when the game does not take the rule', () => {
    // The control: the same play in a game without the rule is a perfectly ordinary six-pointer.
    const res = evaluateLocal(anyWord, 'erudit_ru', contourBoard(), bra, false);
    expect(res.legal).toBe(true);
    expect(res.words).toEqual(['бра']);
    expect(res.score).toBe(6);
    expect(res.repeatedWord).toBeUndefined();
  });

  it('rejects the play and names the word when the game has already used it', () => {
    const res = evaluateLocal(anyWord, 'erudit_ru', contourBoard(), bra, false, played);
    expect(res.legal).toBe(false);
    expect(res.score).toBe(0);
    // Named, so the caption can read "БРА: уже использовано" instead of the bare turn label.
    expect(res.repeatedWord).toBe('бра');
  });

  it('leaves the play alone when the rule is in force but the word is new', () => {
    const res = evaluateLocal(anyWord, 'erudit_ru', contourBoard(), bra, false, playedWordsFromHistory([playRecord('кот')]));
    expect(res.legal).toBe(true);
    expect(res.score).toBe(6);
  });
});

describe('evaluateLocal under the rule with several words per turn', () => {
  // The same position the server engine scores in backend/internal/engine/norepeat_test.go
  // (TestNoRepeatDropsTheScoreOfAReplayedCrossWord), pinned to the same two totals: КОТ stands
  // across the centre and again down column 5, and РОТ laid across row 12 completes the second
  // one as a cross-word. Both engines must agree to the point, or one of the two tests breaks.
  function crossBoard(): Board {
    return place(emptyBoard(), [
      [7, 7, 'к'], [7, 8, 'о'], [7, 9, 'т'], // the opening КОТ, away from the play
      [10, 5, 'к'], [11, 5, 'о'], // the standing КО the play completes
    ]);
  }
  const rot: PlacedTile[] = [tile(12, 3, 'р'), tile(12, 4, 'о'), tile(12, 5, 'т')];

  it('keeps the play legal when only a cross-word repeats, and stops scoring that word', () => {
    const free = evaluateLocal(anyWord, 'erudit_ru', crossBoard(), rot, true);
    expect(free.legal).toBe(true);
    expect(free.words).toEqual(['рот', 'кот']);
    expect(free.score).toBe(10); // РОТ plus the КОТ cross-word

    const restricted = evaluateLocal(anyWord, 'erudit_ru', crossBoard(), rot, true, playedWordsFromHistory([playRecord('кот')]));
    expect(restricted.legal).toBe(true);
    expect(restricted.repeatedWord).toBeUndefined();
    // The cross-word is still formed and still reported — it just earns nothing.
    expect(restricted.words).toEqual(['рот', 'кот']);
    expect(restricted.score).toBe(5);
  });

  it('rejects the play when the main word repeats, whatever its cross-words do', () => {
    const res = evaluateLocal(anyWord, 'erudit_ru', crossBoard(), rot, true, playedWordsFromHistory([playRecord('рот', 'кот')]));
    expect(res.legal).toBe(false);
    expect(res.repeatedWord).toBe('рот');
  });
});
