import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { test } from 'node:test';

import { answerForSeed, fnv1a32, hardModeViolation, scoreGuess } from '../engine';

const ANSWERS: string[] = JSON.parse(
  readFileSync(new URL('../../../../envs/wordle_five/words/answers.json', import.meta.url), 'utf8'),
);

// The same table as envs/wordle_five/tests/test_engine.py. Both suites carry it
// because a vector that only exists on one side is a vector that can silently
// stop being checked on the other.
const VECTORS: [string, string, string][] = [
  ['alloy', 'llama', 'YGYXX'],
  ['speed', 'erase', 'YXYYX'],
  ['array', 'radar', 'YYYGX'],
  ['sassy', 'basis', 'YGGXX'],
  ['eerie', 'rebel', 'YGYXX'],
  ['level', 'eagle', 'YYXYX'],
  ['geese', 'these', 'XXGGG'],
  ['abbey', 'abbot', 'GGGXX'],
  ['crane', 'plane', 'XXGGG'],
  ['alloy', 'balmy', 'YXGXG'],
  ['tares', 'tares', 'GGGGG'],
];

test('scoring vectors', () => {
  for (const [guess, answer, expected] of VECTORS) {
    assert.equal(scoreGuess(guess, answer), expected, `${guess}/${answer}`);
  }
});

test('a letter is never marked more often than it occurs', () => {
  for (const answer of ANSWERS.slice(0, 200)) {
    for (const guess of ANSWERS.slice(0, 50)) {
      const pattern = scoreGuess(guess, answer);
      for (const letter of new Set(guess)) {
        const marked = [...guess].filter((c, i) => c === letter && pattern[i] !== 'X').length;
        const occurs = [...answer].filter((c) => c === letter).length;
        assert.ok(marked <= occurs, `${guess}/${answer} marked ${letter} ${marked}x`);
      }
    }
  }
});

// Pinned identically in envs/wordle_five/tests/test_engine.py. If these two
// lists ever diverge, every ?seed= permalink shows a different puzzle than the
// recorded run it claims to be replaying.
const SEED_VECTORS = [
  'wants', 'amber', 'spume', 'toady', 'divot', 'filly',
  'bobby', 'clews', 'hikes', 'lawns', 'wreak', 'twist',
];

test('seed vectors match the Python engine', () => {
  const got = Array.from({ length: 12 }, (_, s) => answerForSeed(s, ANSWERS));
  assert.deepEqual(got, SEED_VECTORS);
});

test('fnv1a32 matches known values', () => {
  // Computed by envs/wordle_five/wordle_five/engine.py fnv1a32().
  assert.equal(fnv1a32('0'), 0x350ca8af);
  assert.equal(fnv1a32('7'), 0x320ca3f6);
});

test('hard mode locks greens and counts yellows, but does not ban greys', () => {
  assert.equal(hardModeViolation('crown', 'crane', 'GGXXX'), null);
  assert.ok(hardModeViolation('blown', 'crane', 'GGXXX'));

  const twoEs = scoreGuess('speed', 'erase'); // YXYYX
  assert.equal(hardModeViolation('ester', 'speed', twoEs), null);
  assert.ok(hardModeViolation('crest', 'speed', twoEs));

  // Grey letters carry no constraint at all — the rule most implementations
  // add and the real game does not have.
  const cIsGrey = scoreGuess('crane', 'tares');
  assert.equal(cIsGrey[0], 'X');
  assert.equal(hardModeViolation('stare', 'crane', cIsGrey), null);
});

test('conformance digest matches the Python engine', () => {
  const committed = readFileSync(
    new URL('../../../../envs/wordle_five/CONFORMANCE.txt', import.meta.url),
    'utf8',
  ).split(/\s+/)[0];

  const hash = createHash('sha256');
  for (const answer of ANSWERS) {
    hash.update(ANSWERS.map((g) => scoreGuess(g, answer)).join(''));
  }
  assert.equal(hash.digest('hex'), committed);
});
