-- ROBLOX MOVED: expect/utils.lua

local function iterableEquality(a: any, b: any, aStack: Array<any>, bStack: Array<any>): boolean | nil
	aStack = aStack or {}
	bStack = bStack or {}

	if getType(a) ~= "set" or getType(b) ~= "set" then
		return nil
	end

	-- ROBLOX deviation: omitting constructor check

	local length = #aStack
	while length > 0 do
		-- Linear search. Performance is inversely proportional to the number of
		-- unique nested structures.
		-- circular references at same depth are equal
		-- circular reference is not equal to non-circular one
		if aStack[length] == a then
			return bStack[length] == b
		end

		-- ROBLOX deviation: this if check is not included in upstream
		-- if bStack[length] == b then
		-- 	return aStack[length] == a
		-- end

		length -= 1
	end

	table.insert(aStack, a)
	table.insert(bStack, b)

	local function iterableEqualityWithStack(localA: any, localB: any)
		return iterableEquality(localA, localB, { unpack(aStack) }, { unpack(bStack) })
	end

	-- ROBLOX TODO: (ADO-1217) If we eventually have a Map polyfill, we can
	-- expand this to include the Map case as well
	if a.size ~= nil then
		if a.size ~= b.size then
			return false
		elseif isA("set", a) then
			local allFound = true
			for _, aValue in a:ipairs() do
				if not b:has(aValue) then
					local has = false
					for _, bValue in b:ipairs() do
						local isEqual = equals(aValue, bValue, { iterableEqualityWithStack })
						if isEqual == true then
							has = true
						end
					end

					if has == false then
						allFound = false
						break
					end
				end
			end
			table.remove(aStack)
			table.remove(bStack)
			return allFound
		end
	end

	return nil

	-- ROBLOX deviation: omitted section of code for handling the case of a different
	-- kind of iterable not covered by the above Set case
end
