-- ROBLOX deviation: expected does not have Function type annotation
function toThrowExpectedClass(
	matcherName: string,
	options: JestMatcherUtils.MatcherHintOptions,
	thrown: Thrown,
	expected: Function
): SyncExpectationResult
	local function isClass(a)
		return a and getmetatable(a) and getmetatable(a).__index
	end

	local pass = thrown ~= nil and thrown.value ~= nil and instanceof(thrown.value, expected)

	local message
	if pass then
		message = function()
			local retval = matcherHint(matcherName, nil, nil, options)
				.. "\n\n"
				.. printExpectedConstructorNameNot("Expected constructor", expected)
			if
				thrown ~= nil
				and isClass(thrown.value)
				and isClass(expected)
				and getmetatable(thrown.value).__index ~= expected
				and not thrown.value["$$robloxInternalJestError"]
			then
				retval = retval
					.. printReceivedConstructorNameNot("Received constructor", getmetatable(thrown.value), expected)
			end

			retval = retval .. "\n"

			if thrown ~= nil and thrown.hasMessage and not thrown.value["$$robloxInternalJestError"] then
				retval = retval .. formatReceived("Received message: ", thrown, "message") .. formatStack(thrown)
			else
				retval = retval .. formatReceived("Received value: ", thrown, "message") .. formatStack(thrown)
			end

			return retval
		end
	else
		message = function()
			local retval = matcherHint(matcherName, nil, nil, options)
				.. "\n\n"
				.. printExpectedConstructorName("Expected constructor", expected)

			if thrown == nil then
				retval = retval .. "\n" .. DID_NOT_THROW
			else
				if thrown.value ~= nil and isClass(thrown.value) and not thrown.value["$$robloxInternalJestError"] then
					retval = retval .. printReceivedConstructorName("Received constructor", getmetatable(thrown.value))
				end

				retval = retval .. "\n"

				if thrown.hasMessage and not thrown.value["$$robloxInternalJestError"] then
					retval = retval .. formatReceived("Received message: ", thrown, "message") .. formatStack(thrown)
				else
					retval = retval .. formatReceived("Received value: ", thrown, "message") .. formatStack(thrown)
				end
			end

			return retval
		end
	end

	return { message = message, pass = pass }
end

function toThrowExpectedString(
	matcherName: string,
	options: JestMatcherUtils.MatcherHintOptions,
	thrown: Thrown | nil,
	expected: string
): SyncExpectationResult
	local pass = false
	-- ROBLOX deviation: thrown.message can be nil or a table, so ensure it is a string before calling find
	if thrown ~= nil and typeof(thrown.message) == "string" and thrown.message:find(expected, 1, true) then
		pass = true
	end

	local message
	if pass then
		message = function()
			local retval = matcherHint(matcherName, nil, nil, options)
				.. "\n\n"
				.. formatExpected("Expected substring: never ", expected)

			if thrown ~= nil and thrown.hasMessage and not thrown.value["$$robloxInternalJestError"] then
				retval = retval
					.. formatReceived("Received message:         ", thrown, "message", expected)
					.. formatStack(thrown)
			else
				retval = retval
					.. formatReceived("Received value:           ", thrown, "message")
					.. formatStack(thrown)
			end

			return retval
		end
	else
		message = function()
			local retval = matcherHint(matcherName, nil, nil, options)
				.. "\n\n"
				.. formatExpected("Expected substring: ", expected)

			if thrown == nil then
				retval = retval .. "\n" .. DID_NOT_THROW
			else
				if thrown.hasMessage and not thrown.value["$$robloxInternalJestError"] then
					retval = retval .. formatReceived("Received message:   ", thrown, "message") .. formatStack(thrown)
				else
					retval = retval .. formatReceived("Received value:     ", thrown, "message") .. formatStack(thrown)
				end
			end

			return retval
		end
	end

	return { message = message, pass = pass }
end

function toThrow(matcherName: string, options: MatcherHintOptions, thrown: Thrown | nil): SyncExpectationResult
	local pass = thrown ~= nil

	local message

	if pass then
		message = function()
			local retval = matcherHint(matcherName, nil, "", options) .. "\n\n"

			if thrown ~= nil and thrown.hasMessage and not thrown.value["$$robloxInternalJestError"] then
				retval = retval
					.. formatReceived("Error name:    ", thrown, "name")
					.. formatReceived("Error message: ", thrown, "message")
					.. formatStack(thrown)
			else
				retval = retval .. formatReceived("Thrown value: ", thrown, "message") .. formatStack(thrown)
			end

			return retval
		end
	else
		message = function()
			return matcherHint(matcherName, nil, "", options) .. "\n\n" .. DID_NOT_THROW
		end
	end

	return { message = message, pass = pass }
end

function formatExpected(label: string, expected: any)
	return label .. printExpected(expected) .. "\n"
end