local e=require("\064\080\114\111\106\101\099\116\047\065\083\116\097\114\080\097\116\104\102\105\110\100\105\110\103")local M=require("\064\080\097\099\107\097\103\101\115\047\074\101\115\116\071\108\111\098\097\108\115")local v=M.describe local B=M.expect local w=M.it local b=e.Node local Y=e.Grid local f=e.AStar local C=e.containsNode local y=e.getHeuristic v("\078\111\100\101",function()w("\099\114\101\097\116\101\115 \097 \110\111\100\101 \119\105\116\104 \099\111\114\114\101\099\116 \112\114\111\112\101\114\116\105\101\115",function()local e=b({x=5;y=10,walkable=true});(B(e.x)).toBe(5);(B(e.y)).toBe(10);(B(e.walkable)).toBe(true);(B(e.gCost)).toBe(0);(B(e.hCost)).toBe(0);(B(e.parent)).toBe(nil)end)w("\099\097\108\099\117\108\097\116\101\115 \102\067\111\115\116 \099\111\114\114\101\099\116\108\121 \097\115 \103\067\111\115\116 \043 \104\067\111\115\116",function()local e=b({x=1;y=1;walkable=true})e.gCost=10 e.hCost=20;(B(e:getFCost())).toBe(30)end)end)v("\071\114\105\100",function()w("\099\114\101\097\116\101\115 \097 \103\114\105\100 \119\105\116\104 \099\111\114\114\101\099\116 \100\105\109\101\110\115\105\111\110\115 \097\110\100 \110\111\100\101 \112\114\111\112\101\114\116\105\101\115",function()local e=Y({width=5,height=5;blockProbability=.3});(B(e.width)).toBe(5);(B(e.height)).toBe(5)for M=1,e.height,1 do(B(#e.grid[M])).toBe(5)for v=1,e.width,1 do local w=e.grid[M][v];(B(w.x)).toBe(v);(B(w.y)).toBe(M);(B(type(w.walkable))).toBe("\098\111\111\108\101\097\110")end end end)w("\114\101\116\117\114\110\115 \099\111\114\114\101\099\116 \110\101\105\103\104\098\111\114\115 \102\111\114 \097 \110\111\100\101",function()local e=Y({width=3,height=3,blockProbability=0})for M=1,3,1 do for v=1,3,1 do e.grid[M][v].walkable=true end end local M=e:getNode(2,2)assert(M,"\099\101\110\116\101\114\078\111\100\101 \105\115 \110\105\108")local v=e:getNeighbors(M);(B(#v)).toBe(8)local w={{1,1},{2;1},{3;1},{1;2};{3;2},{1;3},{2;3},{3;3}}for e,M in w do local w=false for e,v in v do if v.x==M[1]and v.y==M[2]then w=true break end end;(B(w)).toBe(true)end e.grid[1][1].walkable=false e.grid[2][1].walkable=false e.grid[3][1].walkable=false v=e:getNeighbors(M);(B(#v)).toBe(5)end)w("\114\101\116\114\105\101\118\101\115 \116\104\101 \099\111\114\114\101\099\116 \110\111\100\101 \097\116 \103\105\118\101\110 \099\111\111\114\100\105\110\097\116\101\115",function()local e=Y({width=5;height=5,blockProbability=0})local M=e:getNode(3,4)assert(M,"\110\111\100\101 \105\115 \110\105\108");(B(M.x)).toBe(3);(B(M.y)).toBe(4);(B(e:getNode(0,1))).toBe(nil);(B(e:getNode(1,0))).toBe(nil);(B(e:getNode(6,5))).toBe(nil);(B(e:getNode(5,6))).toBe(nil)end)w("\100\105\115\112\108\097\121\115 \116\104\101 \103\114\105\100 \119\105\116\104\111\117\116 \101\114\114\111\114\115",function()local e=Y({width=3;height=3;blockProbability=0})e:display()end)end)v("\065\083\116\097\114",function()w("\102\105\110\100\115 \097 \112\097\116\104 \119\104\101\110 \111\110\101 \101\120\105\115\116\115",function()local e=Y({width=5;height=5;blockProbability=0})for M=1,5,1 do for v=1,5,1 do e.grid[M][v].walkable=true end end local M=f({grid=e})local v=M:findPath(1,1,5,5)assert(v,"\112\097\116\104 \105\115 \110\105\108")local w=#v>0;(B(w)).toBe(true);(B(v[1].x)).toBe(1);(B(v[1].y)).toBe(1);(B(v[#v].x)).toBe(5);(B(v[#v].y)).toBe(5)for e=1,#v-1,1 do local M=math.abs(v[e].x-v[e+1].x)local w=math.abs(v[e].y-v[e+1].y);(B(M<=1 and w<=1)).toBe(true)end end)w("\102\105\110\100\115 \097 \112\097\116\104 \097\114\111\117\110\100 \111\098\115\116\097\099\108\101\115",function()local e=Y({width=5,height=5;blockProbability=0})for M=1,5,1 do for v=1,5,1 do e.grid[M][v].walkable=true end end e.grid[1][3].walkable=false e.grid[2][3].walkable=false e.grid[3][3].walkable=false e.grid[4][3].walkable=false local M=f({grid=e})local v=M:findPath(1,3,5,3)assert(v,"\112\097\116\104 \105\115 \110\105\108");(B(v[1].x)).toBe(1);(B(v[1].y)).toBe(3);(B(v[#v].x)).toBe(5);(B(v[#v].y)).toBe(3)for e,M in v do(B(M.walkable)).toBe(true)end end)w("\114\101\116\117\114\110\115 \110\105\108 \119\104\101\110 \110\111 \112\097\116\104 \101\120\105\115\116\115",function()local e=Y({width=5,height=5;blockProbability=0})for M=1,5,1 do for v=1,5,1 do local B=e:getNode(v,M)if B then B.walkable=true end end end for M=1,5,1 do e.grid[M][3].walkable=false end local M=f({grid=e})local v=M:findPath(1,1,5,1);(B(v)).toBe(nil)end)w("\114\101\116\117\114\110\115 \097 \115\105\110\103\108\101-\110\111\100\101 \112\097\116\104 \119\104\101\110 \115\116\097\114\116 \097\110\100 \101\110\100 \097\114\101 \116\104\101 \115\097\109\101",function()local e=Y({width=1;height=1;blockProbability=0})e.grid[1][1].walkable=true local M=f({grid=e})local v=M:findPath(1,1,1,1)assert(v,"\112\097\116\104 \105\115 \110\105\108");(B(#v)).toBe(1);(B(v[1].x)).toBe(1);(B(v[1].y)).toBe(1)end)w("\099\111\114\114\101\099\116\108\121 \099\104\101\099\107\115 \105\102 \097 \108\105\115\116 \099\111\110\116\097\105\110\115 \097 \110\111\100\101",function()local e=Y({width=1,height=1;blockProbability=0})local M=f({grid=e})local v=e:getNode(1,1)assert(v,"\110\111\100\101\049 \105\115 \110\105\108")local w=b({x=2;y=2;walkable=true})local y={v};(B(C(y,v))).toBe(true);(B(C(y,w))).toBe(false)end)w("\099\097\108\099\117\108\097\116\101\115 \116\104\101 \104\101\117\114\105\115\116\105\099 \099\111\114\114\101\099\116\108\121 \117\115\105\110\103 \077\097\110\104\097\116\116\097\110 \100\105\115\116\097\110\099\101",function()local e=Y({width=5;height=5;blockProbability=0})local M=f({grid=e})local v=e:getNode(1,1)assert(v,"\110\111\100\101\049 \105\115 \110\105\108")local w=e:getNode(3,4)assert(w,"\110\111\100\101\050 \105\115 \110\105\108")local b=y(v,w);(B(b)).toBe(math.abs(-2)+math.abs(-3))end)end)
--------------------------------------------------------------------------------
-- AStarPathfinding.spec
-- Unit tests for the A* pathfinding module
-- @Daemon6109 (Matthew Radulovich)
--------------------------------------------------------------------------------

local AStarPathfinding = require("@Project/AStarPathfinding")
local JestGlobals = require("@Packages/JestGlobals")

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it

local Node = AStarPathfinding.Node
local Grid = AStarPathfinding.Grid
local AStar = AStarPathfinding.AStar

local containsNode = AStarPathfinding.containsNode
local getHeuristic = AStarPathfinding.getHeuristic

-- Node Class Tests
describe("Node", function()
	it("creates a node with correct properties", function()
		local node = Node({ x = 5, y = 10, walkable = true })
		expect(node.x).toBe(5)
		expect(node.y).toBe(10)
		expect(node.walkable).toBe(true)
		expect(node.gCost).toBe(0)
		expect(node.hCost).toBe(0)
		expect(node.parent).toBe(nil)
	end)

	it("calculates fCost correctly as gCost + hCost", function()
		local node = Node({ x = 1, y = 1, walkable = true })
		node.gCost = 10
		node.hCost = 20
		expect(node:getFCost()).toBe(30)
	end)
end)

-- Grid Class Tests
describe("Grid", function()
	it("creates a grid with correct dimensions and node properties", function()
		local grid = Grid({ width = 5, height = 5, blockProbability = 0.3 })
		expect(grid.width).toBe(5)
		expect(grid.height).toBe(5)

		for y = 1, grid.height do
			expect(#grid.grid[y]).toBe(5) -- Number of columns in each row
			for x = 1, grid.width do
				local node = grid.grid[y][x]
				expect(node.x).toBe(x)
				expect(node.y).toBe(y)
				expect(type(node.walkable)).toBe("boolean")
			end
		end
	end)

	it("returns correct neighbors for a node", function()
		local grid = Grid({ width = 3, height = 3, blockProbability = 0 })
		-- Make all nodes walkable for a controlled test
		for y = 1, 3 do
			for x = 1, 3 do
				grid.grid[y][x].walkable = true
			end
		end

		local centerNode = grid:getNode(2, 2)
		assert(centerNode, "centerNode is nil")
		local neighbors = grid:getNeighbors(centerNode)
		expect(#neighbors).toBe(8) -- Center node should have 8 neighbors

		-- Expected neighbor coordinates
		local expectedCoords = {
			{ 1, 1 },
			{ 2, 1 },
			{ 3, 1 },
			{ 1, 2 },
			{ 3, 2 },
			{ 1, 3 },
			{ 2, 3 },
			{ 3, 3 },
		}
		for _, coord in expectedCoords do
			local found = false
			for _, neighbor in neighbors do
				if neighbor.x == coord[1] and neighbor.y == coord[2] then
					found = true
					break
				end
			end
			expect(found).toBe(true)
		end

		-- Test with some non-walkable nodes
		grid.grid[1][1].walkable = false -- Top-left
		grid.grid[2][1].walkable = false -- Top-center
		grid.grid[3][1].walkable = false -- Top-right
		neighbors = grid:getNeighbors(centerNode)
		expect(#neighbors).toBe(5) -- Only bottom and side neighbors remain
	end)

	it("retrieves the correct node at given coordinates", function()
		local grid = Grid({ width = 5, height = 5, blockProbability = 0 })
		local node = grid:getNode(3, 4)
		assert(node, "node is nil")
		expect(node.x).toBe(3)
		expect(node.y).toBe(4)

		-- Test out-of-bounds coordinates
		expect(grid:getNode(0, 1)).toBe(nil)
		expect(grid:getNode(1, 0)).toBe(nil)
		expect(grid:getNode(6, 5)).toBe(nil)
		expect(grid:getNode(5, 6)).toBe(nil)
	end)

	it("displays the grid without errors", function()
		local grid = Grid({ width = 3, height = 3, blockProbability = 0 })
		-- Simply call Display and ensure no errors occur
		grid:display()
	end)
end)

-- AStar Class Tests
describe("AStar", function()
	it("finds a path when one exists", function()
		local grid = Grid({ width = 5, height = 5, blockProbability = 0 })
		-- Make all nodes walkable
		for y = 1, 5 do
			for x = 1, 5 do
				grid.grid[y][x].walkable = true
			end
		end

		local astar = AStar({ grid = grid })
		local path = astar:findPath(1, 1, 5, 5)
		assert(path, "path is nil")
		local isGreatThan0 = #path > 0
		expect(isGreatThan0).toBe(true)
		expect(path[1].x).toBe(1)
		expect(path[1].y).toBe(1)
		expect(path[#path].x).toBe(5)
		expect(path[#path].y).toBe(5)

		-- Check that each step is adjacent (including diagonals)
		for i = 1, #path - 1 do
			local dx = math.abs(path[i].x - path[i + 1].x)
			local dy = math.abs(path[i].y - path[i + 1].y)
			expect(dx <= 1 and dy <= 1).toBe(true)
		end
	end)

	it("finds a path around obstacles", function()
		local grid = Grid({ width = 5, height = 5, blockProbability = 0 })
		-- Make all nodes walkable initially
		for y = 1, 5 do
			for x = 1, 5 do
				grid.grid[y][x].walkable = true
			end
		end
		-- Create a vertical wall at x=3 from y=1 to y=4
		grid.grid[1][3].walkable = false
		grid.grid[2][3].walkable = false
		grid.grid[3][3].walkable = false
		grid.grid[4][3].walkable = false

		local astar = AStar({ grid = grid })
		local path = astar:findPath(1, 3, 5, 3)
		assert(path, "path is nil")
		expect(path[1].x).toBe(1)
		expect(path[1].y).toBe(3)
		expect(path[#path].x).toBe(5)
		expect(path[#path].y).toBe(3)

		-- Ensure path avoids non-walkable nodes
		for _, node in path do
			expect(node.walkable).toBe(true)
		end
	end)

	it("returns nil when no path exists", function()
		local grid = Grid({ width = 5, height = 5, blockProbability = 0 })
		-- Make all nodes walkable initially
		for y = 1, 5 do
			for x = 1, 5 do
				local node = grid:getNode(x, y)
				if node then
					node.walkable = true
				end
			end
		end
		-- Create a vertical wall at x=3 splitting the grid
		for y = 1, 5 do
			grid.grid[y][3].walkable = false
		end

		local astar = AStar({ grid = grid })
		local path = astar:findPath(1, 1, 5, 1)
		expect(path).toBe(nil)
	end)

	it("returns a single-node path when start and end are the same", function()
		local grid = Grid({ width = 1, height = 1, blockProbability = 0 })
		grid.grid[1][1].walkable = true

		local astar = AStar({ grid = grid })
		local path = astar:findPath(1, 1, 1, 1)
		assert(path, "path is nil")
		expect(#path).toBe(1)
		expect(path[1].x).toBe(1)
		expect(path[1].y).toBe(1)
	end)

	it("correctly checks if a list contains a node", function()
		local grid = Grid({ width = 1, height = 1, blockProbability = 0 })
		local astar = AStar({ grid = grid })
		local node1 = grid:getNode(1, 1)
		assert(node1, "node1 is nil")
		local node2 = Node({ x = 2, y = 2, walkable = true })
		local list = { node1 }

		expect(containsNode(list, node1)).toBe(true)
		expect(containsNode(list, node2)).toBe(false)
	end)

	it("calculates the heuristic correctly using Manhattan distance", function()
		local grid = Grid({ width = 5, height = 5, blockProbability = 0 })
		local astar = AStar({ grid = grid })
		local node1 = grid:getNode(1, 1)
		assert(node1, "node1 is nil")
		local node2 = grid:getNode(3, 4)
		assert(node2, "node2 is nil")

		local heuristic = getHeuristic(node1, node2)
		expect(heuristic).toBe(math.abs(1 - 3) + math.abs(1 - 4)) -- 2 + 3 = 5
	end)
end)
