local CorePackages = game:GetService("CorePackages")

local Cryo = require(CorePackages.Cryo)

local root = script.Parent.Parent

local Constants = require(root.Constants)
local valueToString = require(root.util.valueToString)

local EPSILON = 1e-5

local function floatEq(a, b)
	return math.abs(a - b) <= EPSILON
end

local function v3FloatEq(a, b)
	return floatEq(a.X, b.X) and floatEq(a.Y, b.Y) and floatEq(a.Z, b.Z)
end

local function c3FloatEq(a, b)
	return floatEq(a.r, b.r) and floatEq(a.g, b.g) and floatEq(a.b, b.b)
end

local function propEq(propValue, expectedValue)
	local valueType = typeof(expectedValue)
	if expectedValue == Cryo.None then
		return propValue == nil
	elseif valueType == "number" then
		return floatEq(propValue, expectedValue)
	elseif valueType == "Vector3" then
		return v3FloatEq(propValue, expectedValue)
	elseif valueType == "Color3" then
		return c3FloatEq(propValue, expectedValue)
	else
		return propValue == expectedValue
	end
end

local function validateProperties(instance)

	-- full tree of instance + descendants
	local objects = instance:GetDescendants()
	table.insert(objects, instance)

	for _, object in pairs(objects) do
		for className, properties in pairs(Constants.PROPERTIES) do
			if object:IsA(className) then
				for propName, expectedValue in pairs(properties) do
					-- ensure property exists first
					local propExists, propValue = pcall(function() return object[propName] end)

					if not propExists then
						return false, {
							string.format("Property %s does not exist on type %s", propName, object.ClassName)
						}
					end

					if not propEq(propValue, expectedValue) then
						return false, {
							string.format("Expected %s.%s to be %s", object:GetFullName(), propName, valueToString(expectedValue))
						}
					end
				end
			end
		end
	end

	return true
end

return validateProperties