import { describe, it, expect } from 'vitest';
import {
validatePosition,
validateGridSize,
validateObstacleType,
validatePlaceableTileType,
validateDirection,
validatePuzzleData,
validateNotOnStartOrGoal,
validateMaxObstacles,
validateGoalPosition,
validateStartGoalDifferent,
} from './validator.js';
import type { DraftState } from './types.js';
describe('validatePosition', () => {
it('should accept valid interior position', () => {
expect(validatePosition(1, 1, 10, 10).valid).toBe(true);
expect(validatePosition(5, 5, 10, 10).valid).toBe(true);
expect(validatePosition(8, 8, 10, 10).valid).toBe(true);
});
it('should reject wall positions', () => {
expect(validatePosition(0, 1, 10, 10).valid).toBe(false);
expect(validatePosition(1, 0, 10, 10).valid).toBe(false);
expect(validatePosition(9, 1, 10, 10).valid).toBe(false);
expect(validatePosition(1, 9, 10, 10).valid).toBe(false);
});
it('should reject negative positions', () => {
expect(validatePosition(-1, 1, 10, 10).valid).toBe(false);
expect(validatePosition(1, -1, 10, 10).valid).toBe(false);
});
it('should reject positions beyond grid', () => {
expect(validatePosition(10, 1, 10, 10).valid).toBe(false);
expect(validatePosition(1, 10, 10, 10).valid).toBe(false);
});
});
describe('validateGridSize', () => {
it('should accept valid sizes', () => {
expect(validateGridSize(5, 5).valid).toBe(true);
expect(validateGridSize(10, 10).valid).toBe(true);
expect(validateGridSize(25, 25).valid).toBe(true);
});
it('should reject too small', () => {
expect(validateGridSize(4, 5).valid).toBe(false);
expect(validateGridSize(5, 4).valid).toBe(false);
});
it('should reject too large', () => {
expect(validateGridSize(26, 25).valid).toBe(false);
expect(validateGridSize(25, 26).valid).toBe(false);
});
});
describe('validateObstacleType', () => {
it('should accept valid types', () => {
expect(validateObstacleType('rock')).toBe(true);
expect(validateObstacleType('lava')).toBe(true);
expect(validateObstacleType('hot_coals')).toBe(true);
expect(validateObstacleType('spike')).toBe(true);
expect(validateObstacleType('wall')).toBe(true);
expect(validateObstacleType('barrier')).toBe(true);
});
it('should reject invalid types', () => {
expect(validateObstacleType('ice')).toBe(false);
expect(validateObstacleType('goal')).toBe(false);
expect(validateObstacleType('dirt')).toBe(false);
expect(validateObstacleType('')).toBe(false);
});
});
describe('validatePlaceableTileType', () => {
it('should accept valid placeable types', () => {
expect(validatePlaceableTileType('rock')).toBe(true);
expect(validatePlaceableTileType('lava')).toBe(true);
expect(validatePlaceableTileType('hot_coals')).toBe(true);
expect(validatePlaceableTileType('spike')).toBe(true);
});
it('should reject non-placeable types', () => {
expect(validatePlaceableTileType('wall')).toBe(false);
expect(validatePlaceableTileType('barrier')).toBe(false);
expect(validatePlaceableTileType('thin_ice')).toBe(false);
});
});
describe('validatePuzzleData', () => {
const validPuzzle = {
id: 'test',
name: 'Test',
theme: 'ice',
width: 10,
height: 10,
par: 5,
start: { x: 1, y: 8 },
goal: { x: 8, y: 1 },
obstacles: [{ x: 3, y: 3, type: 'rock' }],
};
it('should accept valid puzzle data', () => {
expect(validatePuzzleData(validPuzzle).valid).toBe(true);
});
it('should reject null', () => {
expect(validatePuzzleData(null).valid).toBe(false);
});
it('should reject missing id', () => {
const { id, ...rest } = validPuzzle;
expect(validatePuzzleData(rest).valid).toBe(false);
});
it('should reject invalid grid size', () => {
expect(validatePuzzleData({ ...validPuzzle, width: 3 }).valid).toBe(false);
});
it('should reject start on wall', () => {
expect(validatePuzzleData({ ...validPuzzle, start: { x: 0, y: 1 } }).valid).toBe(false);
});
it('should allow edge goals (non-corner)', () => {
expect(validatePuzzleData({ ...validPuzzle, goal: { x: 0, y: 5 } }).valid).toBe(true);
expect(validatePuzzleData({ ...validPuzzle, goal: { x: 9, y: 5 } }).valid).toBe(true);
});
it('should reject corner goals', () => {
expect(validatePuzzleData({ ...validPuzzle, goal: { x: 0, y: 0 } }).valid).toBe(false);
});
it('should reject identical start and goal', () => {
expect(validatePuzzleData({ ...validPuzzle, goal: { ...validPuzzle.start } }).valid).toBe(false);
});
it('should reject invalid obstacle type', () => {
expect(validatePuzzleData({ ...validPuzzle, obstacles: [{ x: 3, y: 3, type: 'banana' }] }).valid).toBe(false);
});
it('should reject legacy dirtTiles encoding', () => {
expect(
validatePuzzleData({
...validPuzzle,
dirtTiles: [{ x: 3, y: 3 }],
})
).toEqual({
valid: false,
error: 'dirtTiles is no longer supported',
});
});
});
describe('validateNotOnStartOrGoal', () => {
const draft = {
startPosition: { x: 1, y: 8 },
goalPosition: { x: 8, y: 1 },
} as DraftState;
it('should reject placing on start', () => {
expect(validateNotOnStartOrGoal(1, 8, draft).valid).toBe(false);
});
it('should reject placing on goal', () => {
expect(validateNotOnStartOrGoal(8, 1, draft).valid).toBe(false);
});
it('should accept other positions', () => {
expect(validateNotOnStartOrGoal(5, 5, draft).valid).toBe(true);
});
});
describe('validateGoalPosition', () => {
it('should reject corners', () => {
expect(validateGoalPosition(0, 0, 10, 10).valid).toBe(false);
expect(validateGoalPosition(9, 0, 10, 10).valid).toBe(false);
expect(validateGoalPosition(0, 9, 10, 10).valid).toBe(false);
expect(validateGoalPosition(9, 9, 10, 10).valid).toBe(false);
});
it('should reject out-of-bounds', () => {
expect(validateGoalPosition(-1, 5, 10, 10).valid).toBe(false);
expect(validateGoalPosition(10, 5, 10, 10).valid).toBe(false);
expect(validateGoalPosition(5, -1, 10, 10).valid).toBe(false);
expect(validateGoalPosition(5, 10, 10, 10).valid).toBe(false);
});
it('should allow edges and return isEdge: true', () => {
const result1 = validateGoalPosition(0, 5, 10, 10);
expect(result1.valid).toBe(true);
expect(result1.isEdge).toBe(true);
const result2 = validateGoalPosition(9, 5, 10, 10);
expect(result2.valid).toBe(true);
expect(result2.isEdge).toBe(true);
const result3 = validateGoalPosition(5, 0, 10, 10);
expect(result3.valid).toBe(true);
expect(result3.isEdge).toBe(true);
const result4 = validateGoalPosition(5, 9, 10, 10);
expect(result4.valid).toBe(true);
expect(result4.isEdge).toBe(true);
});
it('should allow interior positions and return isEdge: false', () => {
const result1 = validateGoalPosition(5, 5, 10, 10);
expect(result1.valid).toBe(true);
expect(result1.isEdge).toBe(false);
const result2 = validateGoalPosition(1, 1, 10, 10);
expect(result2.valid).toBe(true);
expect(result2.isEdge).toBe(false);
const result3 = validateGoalPosition(8, 8, 10, 10);
expect(result3.valid).toBe(true);
expect(result3.isEdge).toBe(false);
});
});
describe('validateStartGoalDifferent', () => {
it('should reject same position', () => {
const result = validateStartGoalDifferent(5, 5, 5, 5);
expect(result.valid).toBe(false);
expect(result.error).toContain('cannot be the same');
});
it('should allow different positions', () => {
expect(validateStartGoalDifferent(1, 8, 8, 1).valid).toBe(true);
expect(validateStartGoalDifferent(1, 1, 2, 1).valid).toBe(true);
expect(validateStartGoalDifferent(1, 1, 1, 2).valid).toBe(true);
});
});