import fc from "fast-check"; import { describe, expect, it } from "vitest"; import { MAX_DOCUMENT_BYTES, SUDOKU_DOCUMENT_SCHEMA, SudokuFormatError, decodePuzzleHash, encodePuzzleHash, normalizeSudokuDocument, parsePlainGrid, parseSudokuDocument, serializePlainGrid, serializeSudokuDocument, type SudokuDocument, } from "../../src/formats"; function document(givens: readonly number[], size = 9): SudokuDocument { return { schema: SUDOKU_DOCUMENT_SCHEMA, version: 1, size, givens, constraints: [], }; } describe("Sudoku Tools document format", () => { it("normalizes and round-trips a versioned document", () => { const source: SudokuDocument = { ...document(Array(81).fill(0)), title: "Local puzzle", solution: Array.from({ length: 81 }, (_, index) => (index % 9) + 1), candidates: Array.from({ length: 81 }, () => [3, 1, 3]), cornerMarks: Array.from({ length: 81 }, () => [8, 2, 8]), centerMarks: Array.from({ length: 81 }, () => [7, 4]), colors: Array.from({ length: 81 }, (_, index) => index % 9), elapsedMs: 12_345, constraints: [{ type: "killer-cage", cells: [0, 1], sum: 3 }], }; const parsed = parseSudokuDocument(serializeSudokuDocument(source)); expect(parsed).toEqual({ ...source, candidates: Array.from({ length: 81 }, () => [1, 3]), cornerMarks: Array.from({ length: 81 }, () => [2, 8]), centerMarks: Array.from({ length: 81 }, () => [4, 7]), }); }); it("rejects unsupported versions and oversized input", () => { expect(() => normalizeSudokuDocument({ ...document(Array(81).fill(0)), version: 2 }), ).toThrow(/Unsupported puzzle document version/u); expect(() => parseSudokuDocument(" ".repeat(MAX_DOCUMENT_BYTES + 1)), ).toThrowError(SudokuFormatError); expect(() => normalizeSudokuDocument({ ...document([1, ...Array(15).fill(0)], 4), values: [2, ...Array(15).fill(0)], }), ).toThrow(/preserve its given digit/u); }); it("round-trips compact share hashes", () => { const source = { ...document(Array(81).fill(0)), title: "Hash # & Unicode 🧩", constraints: [{ type: "diagonal", direction: "main" } as const], }; const hash = encodePuzzleHash(source); expect(hash).toMatch(/^#sudoku=v1\./u); expect(decodePuzzleHash(`https://example.invalid/tools/${hash}`)).toEqual( source, ); }); it("round-trips grids for every supported size and symbol", () => { fc.assert( fc.property( fc.integer({ min: 4, max: 16 }).chain((size) => fc .array(fc.integer({ min: 0, max: size }), { minLength: size * size, maxLength: size * size, }) .map((givens) => ({ size, givens })), ), ({ size, givens }) => { const text = serializePlainGrid(document(givens, size)); expect(parsePlainGrid(text)).toMatchObject({ size, givens }); }, ), { numRuns: 80 }, ); }); it("accepts human grid separators and diagnoses bad lengths", () => { const row = "1 . . 4"; const parsed = parsePlainGrid(`${row}\n${row}\n${row}\n${row}`); expect(parsed.size).toBe(4); expect(parsed.givens.slice(0, 4)).toEqual([1, 0, 0, 4]); expect(() => parsePlainGrid("1234")).toThrow(/side length/u); }); });