-- ROBLOX deviation: toBeUndefined equivalent to toBeNil
local function toBeUndefined(
	-- ROBLOX deviation: self param in Lua needs to be explicitely defined
	self: MatcherState,
	received: any,
	expected: nil
)
	local matcherName = "toBeUndefined"
	local options: MatcherHintOptions = {
		isNot = self.isNot,
		promise = self.promise,
	}
	ensureNoExpected(expected, matcherName, options)

	local pass = received == nil

	local message = function()
		return matcherHint(matcherName, nil, "", options)
			.. "\n\n"
			.. string.format("Received: %s", printReceived(received))
	end

	return { message = message, pass = pass }
end

local function toContain(
	-- ROBLOX deviation: self param in Lua needs to be explicitely defined
	self: MatcherState,
	received: any,
	expected: any
)
	local matcherName = "toContain"
	local isNot = self.isNot
	local options: MatcherHintOptions = {
		comment = "string.find or table.find",
		isNot = isNot,
		promise = self.promise,
	}

	if received == nil then
		error(
			Error(
				matcherErrorMessage(
					matcherHint(matcherName, nil, nil, options),
					string.format("%s value must not be nil", RECEIVED_COLOR("received")),
					printWithType("Received", received, printReceived)
				)
			)
		)
	end

	if typeof(received) == "string" then
		local wrongTypeErrorMessage = ("%s value must be a string if %s value is a string"):format(
			EXPECTED_COLOR("expected"),
			RECEIVED_COLOR("received")
		)
		if typeof(expected) ~= "string" then
			error(
				Error.new(
					matcherErrorMessage(
						matcherHint(matcherName, received, tostring(expected), options),
						wrongTypeErrorMessage,
						tostring(printWithType("Expected", expected, printExpected))
							.. "\n"
							.. tostring(printWithType("Received", received, printReceived))
					)
				)
			)
		end

		local index = received:find(tostring(expected), 1, true)
		local pass = index ~= nil

		local message = function()
			local labelExpected = string.format("Expected %s", typeof(expected) == "string" and "substring" or "value")
			local labelReceived = "Received string"
			local printLabel = getLabelPrinter(labelExpected, labelReceived)

			return matcherHint(matcherName, nil, nil, options)
				.. "\n\n"
				.. string.format(
					"%s%s%s\n",
					printLabel(labelExpected),
					isNot and "never " or "",
					printExpected(expected)
				)
				.. string.format(
					"%s%s%s",
					printLabel(labelReceived),
					isNot and "      " or "",
					isNot and printReceivedStringContainExpectedSubstring(received, index, #tostring(expected))
						or printReceived(received)
				)
		end

		return { message = message, pass = pass }
	end

	local indexable = Array.from(received)
	local index = table.find(indexable, expected)
	local pass = index ~= nil

	local message = function()
		local labelExpected = "Expected value"
		local labelReceived = string.format("Received %s", getType(received))
		local printLabel = getLabelPrinter(labelExpected, labelReceived)

		local retval = matcherHint(matcherName, nil, nil, options)
			.. "\n\n"
			.. string.format(
				"%s%s%s\n",
				printLabel(labelExpected),
				isNot and "never " or "",
				printExpected(expected)
			)
			.. string.format("%s%s", printLabel(labelReceived), isNot and "      " or "")
		if isNot and Array.isArray(received) then
			retval = retval .. printReceivedArrayContainExpectedItem(received, index)
		else
			retval = retval .. printReceived(received)
		end
		if
			not isNot
			and Array.findIndex(received, function(item)
					return equals(item, expected, { iterableEquality })
				end)
				~= -1
		then
			retval = retval .. string.format("\n\n%s", SUGGEST_TO_CONTAIN_EQUAL)
		end

		return retval
	end

	return { message = message, pass = pass }
end

local function toContainEqual(
	-- ROBLOX deviation: self param in Lua needs to be explicitely defined
	self: MatcherState,
	received: any,
	expected: any
)
	local matcherName = "toContainEqual"
	local isNot = self.isNot
	local options: MatcherHintOptions = {
		comment = "deep equality",
		isNot = isNot,
		promise = self.promise,
	}

	if received == nil then
		error(
			Error(
				matcherErrorMessage(
					matcherHint(matcherName, nil, nil, options),
					string.format("%s value must not be nil", RECEIVED_COLOR("received")),
					printWithType("Received", received, printReceived)
				)
			)
		)
	end

	local index = Array.findIndex(Array.from(received), function(item)
		return equals(item, expected, { iterableEquality })
	end)
	local pass = index ~= -1

	local message = function()
		local labelExpected = "Expected value"
		local labelReceived = string.format("Received %s", getType(received))
		local printLabel = getLabelPrinter(labelExpected, labelReceived)

		local retval = matcherHint(matcherName, nil, nil, options)
			.. "\n\n"
			.. string.format(
				"%s%s%s\n",
				printLabel(labelExpected),
				isNot and "never " or "",
				printExpected(expected)
			)
			.. string.format("%s%s", printLabel(labelReceived), isNot and "      " or "")
		if isNot and Array.isArray(received) then
			retval = retval .. printReceivedArrayContainExpectedItem(received, index)
		else
			retval = retval .. printReceived(received)
		end

		return retval
	end

	return { message = message, pass = pass }
end

local function toEqual(
	-- ROBLOX deviation: self param in Lua needs to be explicitely defined
	self: MatcherState,
	received: any,
	expected: any
)
	local matcherName = "toEqual"
	local options: MatcherHintOptions = {
		comment = "deep equality",
		isNot = self.isNot,
		promise = self.promise,
	}

	local pass = equals(received, expected, { iterableEquality })

	local message
	if pass then
		message = function()
			local retval = matcherHint(matcherName, nil, nil, options)
				.. "\n\n"
				.. string.format("Expected: never %s\n", printExpected(expected))
			if stringify(expected) ~= stringify(received) then
				retval = retval .. string.format("Received:       %s", printReceived(received))
			end
			return retval
		end
	else
		message = function()
			return matcherHint(matcherName, nil, nil, options)
				.. "\n\n"
				.. printDiffOrStringify(expected, received, EXPECTED_LABEL, RECEIVED_LABEL, isExpand(self.expand))
		end
	end

	-- Passing the actual and expected objects so that a custom reporter
	-- could access them, for example in order to display a custom visual diff,
	-- or create a different error message
	return { actual = received, expected = expected, message = message, name = matcherName, pass = pass }
end

local function toHaveLength(
	-- ROBLOX deviation: self param in Lua needs to be explicitely defined
	self: MatcherState,
	received: any,
	expected: number
)
	local matcherName = "toHaveLength"
	local isNot = self.isNot
	local options: MatcherHintOptions = {
		isNot = isNot,
		promise = self.promise,
	}

	-- ROBLOX deviation: only strings and array-like tables have a well defined built in length in Lua
	-- we also allow for objects that have a length property
	local hasLengthProperty = typeof(received) == "table" and typeof(received.length) == "number"
	if not Array.isArray(received) and typeof(received) ~= "string" and not hasLengthProperty then
		error(
			Error(
				matcherErrorMessage(
					matcherHint(matcherName, nil, nil, options),
					string.format(
						"%s value must have a length property whose value must be a number",
						RECEIVED_COLOR("received")
					),
					printWithType("Received", received, printReceived)
				)
			)
		)
	end

	ensureExpectedIsNonNegativeInteger(expected, matcherName, options)

	local pass
	local receivedLength
	if received.length ~= nil then
		receivedLength = received.length
		pass = receivedLength == expected
	else
		receivedLength = #received
		pass = receivedLength == expected
	end

	local message = function()
		local labelExpected = "Expected length"
		local labelReceivedLength = "Received length"
		local labelReceivedValue = string.format("Received %s", getType(received))
		local printLabel = getLabelPrinter(labelExpected, labelReceivedLength, labelReceivedValue)

		local retval = matcherHint(matcherName, nil, nil, options)
			.. "\n\n"
			.. string.format(
				"%s%s%s\n",
				printLabel(labelExpected),
				isNot and "never " or "",
				printExpected(expected)
			)
		if not isNot then
			retval = retval .. string.format("%s%s\n", printLabel(labelReceivedLength), printReceived(receivedLength))
		end

		return retval
			.. string.format(
				"%s%s%s",
				printLabel(labelReceivedValue),
				isNot and "      " or "",
				printReceived(received)
			)
	end

	return { message = message, pass = pass }
end

local function toHaveProperty(
	-- ROBLOX deviation: self param in Lua needs to be explicitely defined
	self: MatcherState,
	received: any,
	expectedPath: any,
	expectedValue: any?
)
	local matcherName = "toHaveProperty"
	local expectedArgument = "path"
	local hasValue = expectedValue ~= nil
	local options: MatcherHintOptions = {
		isNot = self.isNot,
		promise = self.promise,
		secondArgument = hasValue and "value" or "",
	}

	if received == nil then
		error(
			Error(
				matcherErrorMessage(
					matcherHint(matcherName, nil, expectedArgument, options),
					string.format("%s value must not be nil", RECEIVED_COLOR("received")),
					printWithType("Received", received, printReceived)
				)
			)
		)
	end

	local expectedPathType = getType(expectedPath)

	if expectedPathType ~= "string" and expectedPathType ~= "table" then
		error(
			Error(
				matcherErrorMessage(
					matcherHint(matcherName, nil, expectedArgument, options),
					string.format("%s path must be a string or array", EXPECTED_COLOR("expected")),
					printWithType("Expected", expectedPath, printExpected)
				)
			)
		)
	end

	local expectedPathLength
	if typeof(expectedPath) == "string" then
		expectedPathLength = #pathAsArray(expectedPath)
	else
		expectedPathLength = #expectedPath
	end

	if expectedPathType == "table" and expectedPathLength == 0 then
		error(
			Error(
				matcherErrorMessage(
					matcherHint(matcherName, nil, expectedArgument, options),
					string.format("%s path must not be an empty array", EXPECTED_COLOR("expected")),
					printWithType("Expected", expectedPath, printExpected)
				)
			)
		)
	end

	local result = getPath(received, expectedPath)
	local lastTraversedObject = result.lastTraversedObject
	local hasEndProp = result.hasEndProp
	local receivedPath = result.traversedPath
	local hasCompletePath = #receivedPath == expectedPathLength

	local receivedValue
	if hasCompletePath then
		receivedValue = result.value
	else
		receivedValue = lastTraversedObject
	end

	local pass
	if hasValue then
		pass = equals(result.value, expectedValue, { iterableEquality })
	else
		pass = not not hasEndProp -- theoretically undefined if empty path
	end
	-- Remove type cast if we rewrite getPath as iterative algorithm.

	-- ROBLOX deviation: omit code block for dealing with upstream edge case with undefined expectedValues

	local message
	if pass then
		message = function()
			local retval = matcherHint(matcherName, nil, expectedArgument, options) .. "\n\n"
			if hasValue then
				retval = retval
					.. string.format("Expected path: %s\n\n", printExpected(expectedPath))
					.. string.format("Expected value: never %s", printExpected(expectedValue))
				if stringify(expectedValue) ~= stringify(receivedValue) then
					return retval .. string.format("\nReceived value:       %s", printReceived(receivedValue))
				end
				return retval
			else
				return retval
					.. string.format("Expected path: never %s\n\n", printExpected(expectedPath))
					.. string.format("Received value: %s", printReceived(receivedValue))
			end
		end
	else
		message = function()
			local retval = matcherHint(matcherName, nil, expectedArgument, options)
				.. "\n\n"
				.. string.format("Expected path: %s\n", printExpected(expectedPath))
			if hasCompletePath then
				return retval
					.. "\n"
					.. printDiffOrStringify(
						expectedValue,
						receivedValue,
						EXPECTED_VALUE_LABEL,
						RECEIVED_VALUE_LABEL,
						isExpand(self.expand)
					)
			end
			retval = retval .. "Received path: "

			if expectedPathType == "table" or #receivedPath == 0 then
				retval = retval .. string.format("%s\n\n", printReceived(receivedPath))
			else
				retval = retval .. string.format("%s\n\n", printReceived(table.concat(receivedPath, ".")))
			end

			if hasValue then
				retval = retval .. string.format("Expected value: %s\n", printExpected(expectedValue))
			end

			return retval .. string.format("Received value: %s", printReceived(receivedValue))
		end
	end

	return { message = message, pass = pass }
end