import { describe, it, expect } from 'vitest';
import { lineFrom, matchWord, cellFromPoint } from './wordsearch.js';

const colinear = (cells) => {
  if (cells.length < 2) return true;
  const [r0, c0] = cells[0], [r1, c1] = cells[1];
  const sr = Math.sign(r1 - r0), sc = Math.sign(c1 - c0);
  return cells.every(([r, c], i) => r === r0 + sr * i && c === c0 + sc * i);
};

describe('lineFrom', () => {
  it('builds straight horizontal / vertical / diagonal runs', () => {
    expect(lineFrom([2, 1], [2, 4], 10)).toEqual([[2, 1], [2, 2], [2, 3], [2, 4]]);
    expect(lineFrom([1, 3], [4, 3], 10)).toEqual([[1, 3], [2, 3], [3, 3], [4, 3]]);
    expect(lineFrom([0, 0], [3, 3], 10)).toEqual([[0, 0], [1, 1], [2, 2], [3, 3]]);
  });

  it('snaps a non-straight drag onto a straight line (never bent)', () => {
    // knight-ish move (dr=1, dc=3) → should snap, and stay colinear
    const cells = lineFrom([5, 2], [6, 8], 10);
    expect(colinear(cells)).toBe(true);
    // a near-diagonal still resolves to a straight path
    expect(colinear(lineFrom([0, 0], [4, 5], 10))).toBe(true);
  });

  it('clamps to the grid and handles a single cell', () => {
    expect(lineFrom([0, 0], [0, 0], 10)).toEqual([[0, 0]]);
    const cells = lineFrom([8, 8], [20, 8], 10);   // runs off the bottom edge
    expect(cells.every(([r, c]) => r >= 0 && r < 10 && c >= 0 && c < 10)).toBe(true);
  });
});

describe('cellFromPoint', () => {
  // pad=7; inner = 462-14 = 448; with n=14 → cw = 32px per column.
  const rect = { left: 0, top: 0, width: 462 };
  const at = (x, y) => cellFromPoint(rect, x, y, 14);
  const colCentre = (col) => 7 + col * 32 + 16; // pad + col*cw + half a cell

  it('maps a cell centre to that cell', () => {
    expect(at(colCentre(0), colCentre(0))).toEqual([0, 0]);
    expect(at(colCentre(7), colCentre(3))).toEqual([3, 7]);
  });

  it('maps the LAST column correctly (the overflow regression)', () => {
    expect(at(colCentre(13), colCentre(13))).toEqual([13, 13]); // not 12, not off-grid
  });

  it('clamps points outside the grid to the nearest cell', () => {
    expect(at(-50, -50)).toEqual([0, 0]);
    expect(at(9999, 9999)).toEqual([13, 13]);
  });

  it('respects a non-zero rect origin (scrolled page)', () => {
    const r2 = { left: 100, top: 200, width: 462 };
    expect(cellFromPoint(r2, 100 + colCentre(5), 200 + colCentre(2), 14)).toEqual([2, 5]);
  });
});

describe('matchWord', () => {
  const grid = ['CALM', 'XXXX', 'XXXX', 'XXXX']; // CALM across row 0
  const words = ['CALM', 'PEACE'];

  it('matches a forward word', () => {
    expect(matchWord([[0, 0], [0, 1], [0, 2], [0, 3]], grid, words, [])).toBe('CALM');
  });

  it('matches a reversed selection to the listed word', () => {
    expect(matchWord([[0, 3], [0, 2], [0, 1], [0, 0]], grid, words, [])).toBe('CALM');
  });

  it('is harmless on an already-found word (no re-record)', () => {
    expect(matchWord([[0, 0], [0, 1], [0, 2], [0, 3]], grid, words, ['CALM'])).toBe(null);
  });

  it('returns null for a non-word or too-short selection', () => {
    expect(matchWord([[1, 0], [1, 1], [1, 2], [1, 3]], grid, words, [])).toBe(null); // XXXX
    expect(matchWord([[0, 0]], grid, words, [])).toBe(null);
    expect(matchWord([], grid, words, [])).toBe(null);
  });
});
