/* Copyright (C) 2025 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * 0 A.D. is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "lib/self_test.h"

#include "lib/types.h"
#include "maths/Fixed.h"
#include "simulation2/helpers/Grid.h"
#include "simulation2/helpers/PathGoal.h"
#include "simulation2/helpers/Pathfinding.h"

#include <cmath>
#include <cstdio>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>

#define TEST
#include "simulation2/helpers/HierarchicalPathfinder.h"

class TestHierarchicalPathfinder : public CxxTest::TestSuite
{
public:
	void setUp()
	{
	}

	void tearDown()
	{
	}

	const pass_class_t PASS_1 = 1;
	const pass_class_t PASS_2 = 2;
	const pass_class_t NON_PASS_1 = 4;

	const u16 mapSize = 240;

	std::map<std::string, pass_class_t> pathClassMask;
	std::map<std::string, pass_class_t> nonPathClassMask;

	void debug_grid(Grid<NavcellData>& grid)
	{
		for (size_t i = 0; i < grid.m_W; ++i)
		{
			for (size_t j = 0; j < grid.m_H; ++j)
				printf("%i", grid.get(i, j));
			printf("\n");
		}
	}

	void debug_grid_points(Grid<NavcellData>& grid, u16 i1, u16 j1, u16 i2, u16 j2)
	{
		for (size_t i = 0; i < grid.m_W; ++i)
		{
			for (size_t j = 0; j < grid.m_H; ++j)
			{
				if (i == i1 && j == j1)
					printf("A");
				else if (i == i2 && j == j2)
					printf("B");
				else
					printf("%i", grid.get(i, j));
			}
			printf("\n");
		}
	}

	void assert_blank(HierarchicalPathfinder& hierPath)
	{
		// test that the map has the same global region everywhere
		HierarchicalPathfinder::GlobalRegionID globalRegionID = hierPath.GetGlobalRegion(35, 23, PASS_1);
		for (u16 i = 0; i < mapSize; ++i)
			for (u16 j = 0; j < mapSize; ++j)
			{
				TS_ASSERT(globalRegionID == hierPath.GetGlobalRegion(i, j, PASS_1));
				TS_ASSERT(hierPath.GetGlobalRegion(i, j, PASS_2) == 0);
			}

		u16 i = 89;
		u16 j = 34;
		hierPath.FindNearestPassableNavcell(i, j, PASS_1);
		TS_ASSERT(i == 89 && j == 34);

		for (auto& chunk : hierPath.m_Chunks[PASS_1])
			TS_ASSERT(chunk.m_RegionsID.size() == 1);

		// number of connected regions: 4 in the middle, 2 in the corners.
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(120, 120, PASS_1)].size() == 4);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(20,  20,  PASS_1)].size() == 2);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(220, 220, PASS_1)].size() == 2);

		std::set<HierarchicalPathfinder::RegionID> reachables;
		hierPath.FindReachableRegions(hierPath.Get(120, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 9);
		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(20, 20, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 9);
	}

	void test_reachability_and_update()
	{
		pathClassMask = std::map<std::string, pass_class_t> {
			{ "1", 1 },
			{ "2", 2 },
		};
		nonPathClassMask = std::map<std::string, pass_class_t> {
			{ "3", 4 }
		};

		HierarchicalPathfinder hierPath;
		Grid<NavcellData> grid(mapSize, mapSize);
		Grid<u8> dirtyGrid(mapSize, mapSize);

		// Entirely passable for PASS_1, not for others;
		for (size_t i = 0; i < mapSize; ++i)
			for (size_t j = 0; j < mapSize; ++j)
				grid.set(i, j, 6);

		hierPath.Recompute(&grid, nonPathClassMask, pathClassMask);

		assert_blank(hierPath);

		//////////////////////////////////////////////////////
		// Split the map in two in the middle.
		for (u16 j = 0; j < mapSize; ++j)
		{
			grid.set(125, j, 7);
			dirtyGrid.set(125, j, 1);
		}

		hierPath.Update(&grid, dirtyGrid);

		// Global region: check we are now split in two.
		TS_ASSERT(hierPath.GetGlobalRegion(50, 50, PASS_1) != hierPath.GetGlobalRegion(150, 50, PASS_1));
		for (u16 j = 0; j < mapSize; ++j)
		{
			TS_ASSERT(hierPath.Get(125, j, PASS_1).r == 0);
			TS_ASSERT(hierPath.GetGlobalRegion(125, j, PASS_1) == 0);
		}
		for (u16 i = 0; i < 125; ++i)
			for (u16 j = 0; j < mapSize; ++j)
			{
				TS_ASSERT(hierPath.GetGlobalRegion(50, 50, PASS_1) == hierPath.GetGlobalRegion(i, j, PASS_1));
				TS_ASSERT(hierPath.GetGlobalRegion(i, j, PASS_2) == 0);
			}
		for (u16 i = 126; i < mapSize; ++i)
			for (u16 j = 0; j < mapSize; ++j)
			{
				TS_ASSERT(hierPath.GetGlobalRegion(150, 50, PASS_1) == hierPath.GetGlobalRegion(i, j, PASS_1));
				TS_ASSERT(hierPath.GetGlobalRegion(i, j, PASS_2) == 0);
			}

		// number of connected regions: 3 in the middle (both sides), 2 in the corners.
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(120, 120, PASS_1)].size() == 3);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(170, 120, PASS_1)].size() == 3);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(20,  20,  PASS_1)].size() == 2);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(220, 220, PASS_1)].size() == 2);

		std::set<HierarchicalPathfinder::RegionID> reachables;
		hierPath.FindReachableRegions(hierPath.Get(120, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 6);
		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(170, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 6);

		//////////////////////////////////////////////////////
		// Un-split the map in two in the middle.
		for (u16 j = 0; j < mapSize; ++j)
		{
			grid.set(125, j, 6);
			dirtyGrid.set(125, j, 1);
		}
		hierPath.Update(&grid, dirtyGrid);
		assert_blank(hierPath);

		//////////////////////////////////////////////////////
		// Partial split in the middle chunk - no actual connectivity change
		for (u16 j = 120; j < 150; ++j)
		{
			grid.set(125, j, 7);
			dirtyGrid.set(125, j, 1);
		}
		hierPath.Update(&grid, dirtyGrid);
		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(170, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 9);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(170, 120, PASS_1)].size() == 4);

		//////////////////////////////////////////////////////
		// Block a strip along the edge, but regions are still connected.
		for (u16 j = 70; j < 200; ++j)
		{
			grid.set(96, j, 7);
			dirtyGrid.set(96, j, 1);
		}
		hierPath.Update(&grid, dirtyGrid);
		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(170, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 9);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(20, 120, PASS_1)].size() == 2);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(170, 120, PASS_1)].size() == 3);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(200, 120, PASS_1)].size() == 3);

		//////////////////////////////////////////////////////
		// Block the other edge
		for (u16 j = 70; j < 200; ++j)
		{
			grid.set(192, j, 7);
			dirtyGrid.set(192, j, 1);
		}
		hierPath.Update(&grid, dirtyGrid);
		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(170, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 9);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(20, 120, PASS_1)].size() == 2);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(170, 120, PASS_1)].size() == 2);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(200, 120, PASS_1)].size() == 2);

		//////////////////////////////////////////////////////
		// Create an isolated region in the middle chunk
		for (u16 i = 96; i < 140; ++i)
		{
			grid.set(i, 110, 7);
			dirtyGrid.set(i, 110, 1);
		}
		for (u16 i = 96; i < 140; ++i)
		{
			grid.set(i, 140, 7);
			dirtyGrid.set(i, 140, 1);
		}
		for (u16 j = 110; j < 141; ++j)
		{
			grid.set(140, j, 7);
			dirtyGrid.set(140, j, 1);
		}
		hierPath.Update(&grid, dirtyGrid);

		TS_ASSERT(hierPath.GetGlobalRegion(120, 120, PASS_1) != hierPath.GetGlobalRegion(150, 50, PASS_1));

		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(170, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 9);
		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(120, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 1);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(120, 120, PASS_1)].size() == 0);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(20, 120, PASS_1)].size() == 2);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(170, 120, PASS_1)].size() == 2);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(200, 120, PASS_1)].size() == 2);

		//////////////////////////////////////////////////////
		// Open it
		for (u16 j = 110; j < 141; ++j)
		{
			grid.set(140, j, 6);
			dirtyGrid.set(140, j, 1);
		}
		hierPath.Update(&grid, dirtyGrid);

		TS_ASSERT(hierPath.GetGlobalRegion(120, 120, PASS_1) == hierPath.GetGlobalRegion(150, 50, PASS_1));
		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(170, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 9);
		reachables.clear();
		hierPath.FindReachableRegions(hierPath.Get(120, 120, PASS_1), reachables, PASS_1);
		TS_ASSERT(reachables.size() == 9);
		TS_ASSERT(hierPath.m_Edges[PASS_1][hierPath.Get(120, 120, PASS_1)].size() == 2);
	}

	u16 manhattan(u16 i, u16 j, u16 gi, u16 gj)
	{
		return abs(i - gi) + abs(j - gj);
	}

	double euclidian(u16 i, u16 j, u16 gi, u16 gj)
	{
		return sqrt((i - gi) * (i - gi) + (j - gj) * (j - gj));
	}

	void test_passability()
	{
		pathClassMask = std::map<std::string, pass_class_t> {
			{ "1", 1 },
			{ "2", 2 },
		};
		nonPathClassMask = std::map<std::string, pass_class_t> {
			{ "3", 4 }
		};

		// 0 is passable, 1 is not.
		// i is vertical, j is horizontal;
#define _ 0
#define X 1
		NavcellData gridDef[40][40] = {
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,X,X,X,X,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,X,_,_,X,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,X,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,X,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,X,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,X,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,X,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,_,X,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,X,X,X,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,X,X,X,X,X,X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
			{_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}
		};
#undef _
#undef X
		// upscaled n times
		const int scale = 6;

		HierarchicalPathfinder hierPath;
		Grid<NavcellData> grid(40*scale, 40*scale);
		Grid<u8> dirtyGrid(40*scale, 40*scale);

		for (size_t i = 0; i < 40; ++i)
			for (size_t j = 0; j < 40; ++j)
				for (size_t ii = 0; ii < scale; ++ii)
					for (size_t jj = 0; jj < scale; ++jj)
						grid.set(i * scale + ii, j * scale + jj, gridDef[i][j]);

		hierPath.Recompute(&grid, nonPathClassMask, pathClassMask);

		u16 i = 5, j = 5;
		hierPath.FindNearestPassableNavcell(i, j, PASS_1);
		TS_ASSERT(i == 5 && j == 5);

		// use a macro so the lines reported by tests are accurate
#define check_closest_passable(i, j, expected_manhattan) \
	oi = i; oj = j; \
	pi = i; pj = j; \
	TS_ASSERT(!IS_PASSABLE(grid.get(pi, pj), PASS_1)); \
	hierPath.FindNearestPassableNavcell(pi, pj, PASS_1); \
\
	if (expected_manhattan == -1) \
	{ \
		TS_ASSERT(oi == pi && oj == pj); \
	} \
	else \
	{ \
		TS_ASSERT(IS_PASSABLE(grid.get(pi, pj), PASS_1)); \
		TS_ASSERT_EQUALS(manhattan(pi, pj, oi, oj), expected_manhattan); \
	}
		u16 oi, oj, pi, pj;

		check_closest_passable(4 * scale, 4 * scale, 1);
		check_closest_passable(4 * scale + 1, 4 * scale + 1, 2);
		check_closest_passable(14 * scale + 2, 7 * scale + 2, 9);
		check_closest_passable(14 * scale + 2, 7 * scale + 4, 8);
		check_closest_passable(14 * scale + 2, 7 * scale + 5, 7);
		check_closest_passable(14 * scale + 2, 7 * scale + 6, 6);
		check_closest_passable(5 * scale + 3, 7 * scale + 2, 3);
#undef check_closest_passable

		PathGoal goal;
		goal.type = PathGoal::POINT;

		// from the left of the C, goal is unreachable, expect closest navcell to goal
		oi = 5 * scale + 3; oj = 3 * scale + 3;
		pi = 5 * scale + 3; pj = 6 * scale + 2; goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);

		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		hierPath.FindNearestPassableNavcell(pi, pj, PASS_1);
		TS_ASSERT_EQUALS(pi, goal.x.ToInt_RoundToNegInfinity());
		TS_ASSERT_EQUALS(pj, goal.z.ToInt_RoundToNegInfinity());

		// random reachable point.
		oi = 5 * scale + 3; oj = 3 * scale + 3;
		pi = 26 * scale + 3; pj = 5 * scale + 2; goal.x = fixed::FromInt(pi) + fixed::FromInt(1)/3; goal.z = fixed::FromInt(pj) + fixed::FromInt(1)/3;
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		TS_ASSERT(pi == goal.x.ToInt_RoundToNegInfinity() && pj == goal.z.ToInt_RoundToNegInfinity());

		// top-left corner
		goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		TS_ASSERT(pi == goal.x.ToInt_RoundToNegInfinity() && pj == goal.z.ToInt_RoundToNegInfinity());

		// near bottom-right corner
		goal.x = fixed::FromInt(pi) + fixed::FromInt(3)/4; goal.z = fixed::FromInt(pj) + fixed::FromInt(3)/4;
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		TS_ASSERT(pi == goal.x.ToInt_RoundToNegInfinity() && pj == goal.z.ToInt_RoundToNegInfinity());

		// Circle
		goal.type = PathGoal::CIRCLE;
		goal.hw = fixed::FromInt(1) / 2;

		// from the left of the C, goal is unreachable, expect closest navcell to goal
		oi = 5 * scale + 3; oj = 3 * scale + 3;
		pi = 5 * scale + 3; pj = 7 * scale + 2; goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		hierPath.FindNearestPassableNavcell(pi, pj, PASS_1);
		TS_ASSERT(pi == goal.x.ToInt_RoundToNegInfinity() && pj == goal.z.ToInt_RoundToNegInfinity());

		// same position, goal is reachable, expect closest navcell to goal
		goal.hw = fixed::FromInt(3);
		goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		hierPath.FindNearestPassableNavcell(pi, pj, PASS_1);
		TS_ASSERT(pi == goal.x.ToInt_RoundToNegInfinity() && pj == goal.z.ToInt_RoundToNegInfinity());

		// Same position, but goal is unreachable and much farther away.
		goal.type = PathGoal::POINT;
		oi = 5 * scale + 3; oj = 3 * scale + 3;
		pi = 34 * scale + 3; pj = 6 * scale + 2; goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		hierPath.FindNearestPassableNavcell(pi, pj, PASS_1);
		TS_ASSERT_EQUALS(pi, goal.x.ToInt_RoundToNegInfinity())
		TS_ASSERT_EQUALS(pj, goal.z.ToInt_RoundToNegInfinity());

		// Square
		goal.type = PathGoal::SQUARE;
		goal.hw = fixed::FromInt(1) / 2;
		goal.hh = fixed::FromInt(1) / 2;

		// from the left of the C, goal is unreachable, expect closest navcell to goal
		oi = 5 * scale + 3; oj = 3 * scale + 3;
		pi = 5 * scale + 3; pj = 7 * scale + 2; goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		hierPath.FindNearestPassableNavcell(pi, pj, PASS_1);
		TS_ASSERT(pi == goal.x.ToInt_RoundToNegInfinity() && pj == goal.z.ToInt_RoundToNegInfinity());

		// same position, goal is reachable, expect closest navcell to goal
		goal.hw = fixed::FromInt(3);
		goal.hh = fixed::FromInt(3);
		goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		hierPath.FindNearestPassableNavcell(pi, pj, PASS_1);
		TS_ASSERT(pi == goal.x.ToInt_RoundToNegInfinity() && pj == goal.z.ToInt_RoundToNegInfinity());

		// Goal is reachable diagonally (1 cell away)
		goal.hw = fixed::FromInt(1);
		goal.hh = fixed::FromInt(1);
		oi = 5 * scale + 3; oj = 3 * scale + 3;
		pi = 5 * scale - 1; pj = 7 * scale + 3; goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);
		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);
		hierPath.FindNearestPassableNavcell(pi, pj, PASS_1);
		TS_ASSERT(pi == goal.x.ToInt_RoundToNegInfinity() && pj == goal.z.ToInt_RoundToNegInfinity());

		// Huge Circle goal, expect point closest to us.
		goal.type = PathGoal::CIRCLE;
		goal.hw = fixed::FromInt(20);

		oi = 5 * scale + 3; oj = 3 * scale + 3;
		pi = 36 * scale + 3; pj = 7 * scale + 2; goal.x = fixed::FromInt(pi); goal.z = fixed::FromInt(pj);

		hierPath.MakeGoalReachable(oi, oj, goal, PASS_1);

		// bit of leeway for cell placement
		TS_ASSERT(std::fabs(euclidian(goal.x.ToInt_RoundToNegInfinity(), goal.z.ToInt_RoundToNegInfinity(), pi, pj)-20) < 1.5f);
		TS_ASSERT(std::fabs(euclidian(goal.x.ToInt_RoundToNegInfinity(), goal.z.ToInt_RoundToNegInfinity(), oi, oj) - euclidian(pi, pj, oi, oj)) < 22.0f);
	}

	void test_regions_flood_fill()
	{
		// Partial test of region inner flood filling.
		// This highlights that internal region IDs can become higher than the number of regions.
		pathClassMask = std::map<std::string, pass_class_t> {
			{ "1", 1 },
			{ "2", 2 },
		};
		nonPathClassMask = std::map<std::string, pass_class_t> {
			{ "3", 4 }
		};

		// 0 is passable, 1 is not.
		// i is vertical, j is horizontal;
#define _ 0
#define X 1
		NavcellData gridDef[5][5] = {
			{X,_,X,_,_},
			{_,_,X,X,_},
			{X,_,X,_,_},
			{_,_,X,X,_},
			{X,_,X,_,_}
		};
#undef _
#undef X
		HierarchicalPathfinder hierPath;
		Grid<NavcellData> grid(5, 5);
		Grid<u8> dirtyGrid(5, 5);
		for (size_t i = 0; i < 5; ++i)
			for (size_t j = 0; j < 5; ++j)
					grid.set(i, j, gridDef[i][j]);
		hierPath.Recompute(&grid, nonPathClassMask, pathClassMask);

		TS_ASSERT_EQUALS(hierPath.m_Chunks[pathClassMask["1"]][0].m_RegionsID.size(), 2);
		TS_ASSERT_EQUALS(hierPath.m_Chunks[pathClassMask["1"]][0].m_RegionsID.back(), 4);
	}
};
