-- ROBLOX upstream: https://github.com/facebook/react/blob/v17.0.1/packages/react/src/__tests__/ReactProfiler-test.internal.js
local React
local ReactFeatureFlags
local ReactNoop
local Scheduler
local ReactCache
local ReactTestRenderer
local ReactTestRendererAct
local _SchedulerTracing
local AdvanceTime
local _AsyncText
local _ComponentWithPassiveEffect
local _Text
local TextResource
local resourcePromise
local setTimeout
local Set

local Packages = script.Parent.Parent.Parent

local ReactGlobals = require(Packages.ReactGlobals)
local JestGlobals = require(Packages.Dev.JestGlobals)
local jestExpect = JestGlobals.expect
local jest = JestGlobals.jest
local describe = JestGlobals.describe
local beforeEach = JestGlobals.beforeEach
local it = JestGlobals.it

local Promise = require(Packages.Dev.Promise)

local function loadModules(config)
	local enableProfilerTimer = (function()
		if config.enableProfilerTimer ~= nil then
			return config.enableProfilerTimer
		end
		return true
	end)()
	local enableProfilerCommitHooks = (function()
		if config.enableProfilerCommitHooks ~= nil then
			return config.enableProfilerCommitHooks
		end
		return true
	end)()
	local enableSchedulerTracing = (function()
		if config.enableSchedulerTracing ~= nil then
			return config.enableSchedulerTracing
		end
		return true
	end)()
	local replayFailedUnitOfWorkWithInvokeGuardedCallback = (function()
		if config.replayFailedUnitOfWorkWithInvokeGuardedCallback ~= nil then
			return config.replayFailedUnitOfWorkWithInvokeGuardedCallback
		end
		return false
	end)()
	local useNoopRenderer = (function()
		if config.useNoopRenderer ~= nil then
			return config.useNoopRenderer
		end
		return false
	end)()
	ReactFeatureFlags = require(Packages.Shared).ReactFeatureFlags

	ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer
	ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks
	ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing
	ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback =
		replayFailedUnitOfWorkWithInvokeGuardedCallback

	local LuauPolyfill = require(Packages.LuauPolyfill)
	setTimeout = LuauPolyfill.setTimeout
	Set = LuauPolyfill.Set

	React = require(script.Parent.Parent)
	Scheduler = require(Packages.Dev.Scheduler)
	_SchedulerTracing = Scheduler.tracing
	ReactCache = require(Packages.Dev.ReactCache)

	if useNoopRenderer then
		ReactNoop = require(Packages.Dev.ReactNoopRenderer)
		ReactTestRenderer = nil
		ReactTestRendererAct = nil
	else
		ReactNoop = nil
		ReactTestRenderer = require(Packages.Dev.ReactTestRenderer)
		ReactTestRendererAct = ReactTestRenderer.unstable_concurrentAct
	end

	AdvanceTime = React.Component:extend("AdvanceTime")
	AdvanceTime.defaultProps = {
		byAmount = 10,
		shouldComponentUpdate = true,
	}
	function AdvanceTime:shouldComponentUpdate(nextProps)
		return nextProps.shouldComponentUpdate
	end
	function AdvanceTime:render()
		-- Simulate time passing when self component is rendered
		Scheduler.unstable_advanceTime(self.props.byAmount)
		return self.props.children or nil
	end

	resourcePromise = nil

	TextResource = ReactCache.unstable_createResource(function(args)
		local text, ms = args[1], args[2] or 0
		resourcePromise = Promise.new(function(resolve, reject)
			setTimeout(function()
				Scheduler.unstable_yieldValue(
					string.format("Promise resolved [%s]", tostring(text))
				)
				resolve(text)
			end, ms)
		end)
		return resourcePromise
	end, function(args)
		local text = args[1]
		return text
	end)

	_AsyncText = function(props)
		local ms, text = props.ms, props.text
		local ok, result = pcall(function()
			TextResource.read({ text, ms })
			Scheduler.unstable_yieldValue(string.format("AsyncText [%s]", text))
			return text
		end)
		if not ok then
			local promise = result
			if typeof(promise.andThen) == "function" then
				Scheduler.unstable_yieldValue(string.format("Suspend [%s]", text))
			else
				Scheduler.unstable_yieldValue(string.format("Error [%s]", text))
			end
			error(promise)
		end
	end

	_Text = function(props)
		local text = props.text
		Scheduler.unstable_yieldValue(string.format("Text [%s]", text))
		return text
	end

	_ComponentWithPassiveEffect = function()
		-- Intentionally schedule a passive effect so the onPostCommit hook will be called.
		React.useEffect(function() end)
		return nil
	end
end

-- ROBLOX Test Noise: in upstream, jest setup config makes these tests hide
-- the error boundary warnings they trigger (scripts/jest/setupTests.js:72)
describe("Profiler", function()
	-- ROBLOX deviation: use faketimers instead
	-- local advanceTimeBy
	-- local currentTime

	describe("works in profiling and non-profiling bundles", function()
		for _, enableSchedulerTracing in { true, false } do
			for _, enableProfilerTimer in { true, false } do
				describe("enableSchedulerTracing:" .. (function()
					if enableSchedulerTracing then
						return "enabled"
					end
					return "disabled"
				end)() .. " enableProfilerTimer:" .. (function()
					if enableProfilerTimer then
						return "enabled"
					end
					return "disabled"
				end)() .. "}", function()
					-- ROBLOX deviation START: add condition, otherwise the suite will fail because of no tests
					if ReactGlobals.__DEV__ and enableProfilerTimer then
						-- ROBLOX deviation END
						beforeEach(function()
							jest.resetModules()

							loadModules({
								enableSchedulerTracing = enableSchedulerTracing,
								enableProfilerTimer = enableProfilerTimer,
								-- ROBLOX TODO: set this explicitly to false until we have the correct HostConfig for the TestRenderer setup
								replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
							})
						end)
						-- ROBLOX deviation START: add condition, otherwise the suite will fail because of no tests
					end
					-- ROBLOX deviation END

					-- This will throw in production too,
					-- But the test is only interested in verifying the DEV error message.
					if ReactGlobals.__DEV__ and enableProfilerTimer then
						it("should warn if required params are missing", function()
							jestExpect(function()
								ReactTestRenderer.create(
									React.createElement(React.Profiler)
								)
							end).toErrorDev(
								'Profiler must specify an "id" as a prop',
								{
									withoutStack = true,
								}
							)
						end)

						it(
							"should support an empty Profiler (with no children)",
							function()
								jestExpect(function()
									ReactTestRenderer.create(
										React.createElement(React.Profiler, {
											id = "label",
											onRender = jest.fn(),
										})
									)
										:toJSON()
									-- ROBLOX TODO: toJSON needs to work, use toMatchSnapshot
								end).never.toThrow()
							end
						)

						it("should render children", function()
							local FunctionComponent = function(props)
								local label = props.label
								return React.createElement("span", nil, label)
							end
							local renderer = ReactTestRenderer.create(
								React.createElement(
									"div",
									nil,
									React.createElement("span", nil, "outside span"),
									React.createElement(
										React.Profiler,
										{ id = "label", onRender = jest.fn() },
										React.createElement("span", nil, "inside span"),
										React.createElement(FunctionComponent, {
											label = "function component",
										})
									)
								)
							)
							jestExpect(function()
								renderer:toJSON()
							end).never.toThrow()
							-- ROBLOX TODO: toJSON needs to work, use toMatchSnapshot
							--toMatchSnapshot()
						end)

						it("should support nested Profilers", function()
							local FunctionComponent = function(props)
								local label = props.label
								return React.createElement("div", nil, label)
							end
							local ClassComponent =
								React.Component:extend("ClassComponent")
							function ClassComponent:render()
								return React.createElement("block", nil, self.props.label)
							end
							local renderer = ReactTestRenderer.create(
								React.createElement(
									React.Profiler,
									{ id = "outer", onRender = jest.fn() },
									React.createElement(FunctionComponent, {
										label = "outer function component",
									}),
									React.createElement(
										React.Profiler,
										{ id = "inner", onRender = jest.fn() },
										React.createElement(ClassComponent, {
											label = "inner class component",
										}),
										React.createElement("span", nil, "inner span")
									)
								)
							)

							jestExpect(function()
								renderer:toJSON()
							end).never.toThrow()
							-- ROBLOX TODO: implement toJSON, use toMatchSnapshot when its available
							-- .toMatchSnapshot()
						end)
					end
				end)
			end
		end
	end)

	for _, enableSchedulerTracing in { true, false } do
		describe("onRender enableSchedulerTracing:" .. (function()
			if enableSchedulerTracing then
				return "enabled"
			end
			return "disabled"
		end)(), function()
			beforeEach(function()
				jest.resetModules()

				loadModules({
					enableSchedulerTracing = enableSchedulerTracing,
					-- ROBLOX TODO: set this explicitly to false until we have the correct HostConfig for the TestRenderer setup
					replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
				})
			end)

			it("should handle errors thrown", function()
				local callback = jest.fn(function(id)
					if id == "throw" then
						error("expected")
					end
				end)

				local didMount = false
				local ClassComponent = React.Component:extend("ClassComponent")
				function ClassComponent:componentDidMount()
					didMount = true
				end
				function ClassComponent:render()
					return self.props.children
				end

				-- Errors thrown from onRender should not break the commit phase,
				-- Or prevent other lifecycles from being called.
				jestExpect(function()
					ReactTestRenderer.create(
						React.createElement(
							ClassComponent,
							nil,
							React.createElement(
								React.Profiler,
								{ id = "do-not-throw", onRender = callback },
								React.createElement(React.Profiler, {
									id = "throw",
									onRender = callback,
								}, React.createElement("div"))
							)
						)
					)
				end).toThrow("expected")
				jestExpect(didMount).toBe(true)
				jestExpect(callback).toHaveBeenCalledTimes(2)
			end)

			it("is not invoked until the commit phase", function()
				local callback = jest.fn()

				local Yield = function(props)
					local value = props.value
					Scheduler.unstable_yieldValue(value)
					return nil
				end

				ReactTestRenderer.create(
					React.createElement(
						React.Profiler,
						{ id = "test", onRender = callback },
						React.createElement(Yield, { value = "first" }),
						React.createElement(Yield, { value = "last" })
					),
					{
						unstable_isConcurrent = true,
					}
				)

				-- Times are logged until a render is committed.
				jestExpect(Scheduler).toFlushAndYieldThrough({ "first" })
				jestExpect(callback).toHaveBeenCalledTimes(0)
				jestExpect(Scheduler).toFlushAndYield({ "last" })
				jestExpect(callback).toHaveBeenCalledTimes(1)
			end)

			-- skipped translating some tests

			it("does not report work done on a sibling", function()
				local callback = jest.fn()

				local DoesNotUpdate = React.memo(function()
					Scheduler.unstable_advanceTime(10)
					return nil
				end, function()
					return true
				end)

				local updateProfilerSibling

				local function ProfilerSibling()
					local count, setCount = React.useState(0)
					updateProfilerSibling = function()
						setCount(count + 1)
					end
					return nil
				end

				local function App()
					return React.createElement(
						React.Fragment,
						nil,
						React.createElement(React.Profiler, {
							id = "test",
							onRender = callback,
						}, React.createElement(DoesNotUpdate)),
						React.createElement(ProfilerSibling)
					)
				end

				local renderer = ReactTestRenderer.create(React.createElement(App))

				jestExpect(callback).toHaveBeenCalledTimes(1)

				local call = callback.mock.calls[1]

				jestExpect(call).toHaveLength((function()
					if enableSchedulerTracing then
						return 7
					else
						return 6
					end
				end)())
				jestExpect(call[1]).toBe("test")
				jestExpect(call[2]).toBe("mount")
				jestExpect(call[3]).toBe(10) -- actual time
				jestExpect(call[4]).toBe(10) -- base time
				jestExpect(call[5]).toBe(0) -- start time
				jestExpect(call[6]).toBe(10) -- commit time
				jestExpect(call[7]).toEqual((function()
					if enableSchedulerTracing then
						return Set.new()
					else
						return nil
					end
				end)()) -- intersection events

				callback:mockReset()

				Scheduler.unstable_advanceTime(20) -- 10 -> 30

				renderer.update(React.createElement(App))

				-- ROBLOX deviation: we don't support dynamic/gated flags, so hard-code the path
				-- 						if (gate(flags => flags.new)) {
				-- None of the Profiler's subtree was rendered because App bailed out before the Profiler.
				-- So we expect onRender not to be called.
				jestExpect(callback).never.toHaveBeenCalled()
				-- 						} else {
				-- Updating a parent reports a re-render,
				-- since React technically did a little bit of work between the Profiler and the bailed out subtree.
				-- This is not optimal but it's how the old reconciler fork works.
				--   jestExpect(callback).toHaveBeenCalledTimes(1)

				--   call = callback.mock.calls[1]

				--   jestExpect(call).toHaveLength((function()
				-- 	if enableSchedulerTracing then
				-- 		return 7
				-- 	else
				-- 		return 6
				-- 	end
				-- 	end)())
				--   jestExpect(call[1]).toBe('test')
				--   jestExpect(call[2]).toBe('update')
				--   jestExpect(call[3]).toBe(0) -- actual time
				--   jestExpect(call[4]).toBe(10) -- base time
				--   jestExpect(call[5]).toBe(30) -- start time
				--   jestExpect(call[6]).toBe(30) -- commit time
				--   jestExpect(call[7]).toEqual((function()
				-- 	if enableSchedulerTracing then
				-- 		return {}
				-- 	else
				-- 		return nil
				-- 	end
				-- end)()) -- intersection events

				--   callback.mockReset()
				-- }

				Scheduler.unstable_advanceTime(20) -- 30 -> 50

				-- Updating a sibling should not report a re-render.
				ReactTestRendererAct(updateProfilerSibling)

				jestExpect(callback).never.toHaveBeenCalled()
			end)

			it("logs render times for both mount and update", function()
				local callback = jest.fn()

				Scheduler.unstable_advanceTime(5) -- 0 -> 5

				local renderer = ReactTestRenderer.create(
					React.createElement(
						React.Profiler,
						{ id = "test", onRender = callback },
						React.createElement(AdvanceTime)
					)
				)

				jestExpect(callback).toHaveBeenCalledTimes(1)

				local call = callback.mock.calls[1]

				jestExpect(call).toHaveLength((function()
					if enableSchedulerTracing then
						return 7
					else
						return 6
					end
				end)())
				jestExpect(call[1]).toBe("test")
				jestExpect(call[2]).toBe("mount")
				jestExpect(call[3]).toBe(10) -- actual time
				jestExpect(call[4]).toBe(10) -- base time
				jestExpect(call[5]).toBe(5) -- start time
				jestExpect(call[6]).toBe(15) -- commit time
				jestExpect(call[7]).toEqual((function()
					if enableSchedulerTracing then
						return Set.new()
					else
						return nil
					end
				end)()) -- intersection events

				callback.mockReset()

				Scheduler.unstable_advanceTime(20) -- 15 -> 35

				renderer.update(React.createElement(React.Profiler, {
					id = "test",
					onRender = callback,
				}, React.createElement(AdvanceTime)))

				jestExpect(callback).toHaveBeenCalledTimes(1)

				call = callback.mock.calls[1]

				jestExpect(call).toHaveLength((function()
					if enableSchedulerTracing then
						return 7
					else
						return 6
					end
				end)())
				jestExpect(call[1]).toBe("test")
				jestExpect(call[2]).toBe("update")
				jestExpect(call[3]).toBe(10) -- actual time
				jestExpect(call[4]).toBe(10) -- base time
				jestExpect(call[5]).toBe(35) -- start time
				jestExpect(call[6]).toBe(45) -- commit time
				jestExpect(call[7]).toEqual((function()
					if enableSchedulerTracing then
						return Set.new()
					else
						return nil
					end
				end)()) -- intersection events

				callback.mockReset()

				Scheduler.unstable_advanceTime(20) -- 45 -> 65

				renderer.update(React.createElement(React.Profiler, {
					id = "test",
					onRender = callback,
				}, React.createElement(AdvanceTime, { byAmount = 4 })))

				jestExpect(callback).toHaveBeenCalledTimes(1)

				call = callback.mock.calls[1]

				jestExpect(call).toHaveLength((function()
					if enableSchedulerTracing then
						return 7
					else
						return 6
					end
				end)())
				jestExpect(call[1]).toBe("test")
				jestExpect(call[2]).toBe("update")
				jestExpect(call[3]).toBe(4) -- actual time
				jestExpect(call[4]).toBe(4) -- base time
				jestExpect(call[5]).toBe(65) -- start time
				jestExpect(call[6]).toBe(69) -- commit time
				jestExpect(call[7]).toEqual((function()
					if enableSchedulerTracing then
						return Set.new()
					else
						return nil
					end
				end)()) -- intersection events
			end)

			it(
				"includes render times of nested Profilers in their parent times",
				function()
					local callback = jest.fn()

					Scheduler.unstable_advanceTime(5) -- 0 -> 5

					ReactTestRenderer.create(
						React.createElement(
							React.Fragment,
							nil,
							React.createElement(
								React.Profiler,
								{ id = "parent", onRender = callback },
								React.createElement(
									AdvanceTime,
									{ byAmount = 10 },
									React.createElement(
										React.Profiler,
										{ id = "child", onRender = callback },
										React.createElement(
											AdvanceTime,
											{ byAmount = 20 }
										)
									)
								)
							)
						)
					)

					jestExpect(callback).toHaveBeenCalledTimes(2)

					-- Callbacks bubble (reverse order).
					local childCall, parentCall =
						callback.mock.calls[1], callback.mock.calls[2]
					jestExpect(childCall[1]).toBe("child")
					jestExpect(parentCall[1]).toBe("parent")

					-- Parent times should include child times
					jestExpect(childCall[3]).toBe(20) -- actual time
					jestExpect(childCall[4]).toBe(20) -- base time
					jestExpect(childCall[5]).toBe(15) -- start time
					jestExpect(childCall[6]).toBe(35) -- commit time
					jestExpect(parentCall[3]).toBe(30) -- actual time
					jestExpect(parentCall[4]).toBe(30) -- base time
					jestExpect(parentCall[5]).toBe(5) -- start time
					jestExpect(parentCall[6]).toBe(35) -- commit time
				end
			)

			it("traces sibling Profilers separately", function()
				local callback = jest.fn()

				Scheduler.unstable_advanceTime(5) -- 0 -> 5

				ReactTestRenderer.create(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(React.Profiler, {
							id = "first",
							onRender = callback,
						}, React.createElement(AdvanceTime, { byAmount = 20 })),
						React.createElement(React.Profiler, {
							id = "second",
							onRender = callback,
						}, React.createElement(AdvanceTime, { byAmount = 5 }))
					)
				)

				jestExpect(callback).toHaveBeenCalledTimes(2)

				-- Callbacks bubble (reverse order).
				local firstCall, secondCall =
					callback.mock.calls[1], callback.mock.calls[2]
				jestExpect(firstCall[1]).toBe("first")
				jestExpect(secondCall[1]).toBe("second")

				-- Parent times should include child times
				jestExpect(firstCall[3]).toBe(20) -- actual time
				jestExpect(firstCall[4]).toBe(20) -- base time
				jestExpect(firstCall[5]).toBe(5) -- start time
				jestExpect(firstCall[6]).toBe(30) -- commit time
				jestExpect(secondCall[3]).toBe(5) -- actual time
				jestExpect(secondCall[4]).toBe(5) -- base time
				jestExpect(secondCall[5]).toBe(25) -- start time
				jestExpect(secondCall[6]).toBe(30) -- commit time
			end)

			it("does not include time spent outside of profile root", function()
				local callback = jest.fn()

				Scheduler.unstable_advanceTime(5) -- 0 -> 5

				ReactTestRenderer.create(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(AdvanceTime, { byAmount = 20 }),
						React.createElement(React.Profiler, {
							id = "test",
							onRender = callback,
						}, React.createElement(AdvanceTime, { byAmount = 5 })),
						React.createElement(AdvanceTime, { byAmount = 20 })
					)
				)

				jestExpect(callback).toHaveBeenCalledTimes(1)

				-- Callbacks bubble (reverse order).
				local call = callback.mock.calls[1]
				jestExpect(call[1]).toBe("test")
				jestExpect(call[3]).toBe(5) -- actual time
				jestExpect(call[4]).toBe(5) -- base time
				jestExpect(call[5]).toBe(25) -- start time
				jestExpect(call[6]).toBe(50) -- commit time
			end)

			it("is not called when blocked by sCU false", function()
				local callback = jest.fn()

				local instance
				local Updater = React.Component:extend("Updater")
				function Updater:init()
					self.state = {}
				end
				function Updater:render()
					instance = self
					return self.props.children
				end

				local renderer = ReactTestRenderer.create(
					React.createElement(
						React.Profiler,
						{ id = "outer", onRender = callback },
						React.createElement(
							Updater,
							nil,
							React.createElement(
								React.Profiler,
								{ id = "inner", onRender = callback },
								React.createElement("div")
							)
						)
					)
				)

				-- All profile callbacks are called for initial render
				jestExpect(callback).toHaveBeenCalledTimes(2)

				callback:mockReset()

				renderer.unstable_flushSync(function()
					instance:setState({
						count = 1,
					})
				end)

				-- Only call onRender for paths that have re-rendered.
				-- Since the Updater's props didn't change,
				-- React does not re-render its children.
				jestExpect(callback).toHaveBeenCalledTimes(1)
				jestExpect(callback.mock.calls[1][1]).toBe("outer")
			end)

			it(
				"decreases actual time but not base time when sCU prevents an update",
				function()
					local callback = jest.fn()

					Scheduler.unstable_advanceTime(5) -- 0 -> 5

					local renderer = ReactTestRenderer.create(
						React.createElement(
							React.Profiler,
							{ id = "test", onRender = callback },
							React.createElement(
								AdvanceTime,
								{ byAmount = 10 },
								React.createElement(
									AdvanceTime,
									{ byAmount = 13, shouldComponentUpdate = false }
								)
							)
						)
					)

					jestExpect(callback).toHaveBeenCalledTimes(1)

					Scheduler.unstable_advanceTime(30) -- 28 -> 58

					renderer.update(
						React.createElement(
							React.Profiler,
							{ id = "test", onRender = callback },
							React.createElement(
								AdvanceTime,
								{ byAmount = 4 },
								React.createElement(
									AdvanceTime,
									{ byAmount = 7, shouldComponentUpdate = false }
								)
							)
						)
					)

					jestExpect(callback).toHaveBeenCalledTimes(2)

					local mountCall, updateCall =
						callback.mock.calls[1], callback.mock.calls[2]

					jestExpect(mountCall[2]).toBe("mount")
					jestExpect(mountCall[3]).toBe(23) -- actual time
					jestExpect(mountCall[4]).toBe(23) -- base time
					jestExpect(mountCall[5]).toBe(5) -- start time
					jestExpect(mountCall[6]).toBe(28) -- commit time

					jestExpect(updateCall[2]).toBe("update")
					jestExpect(updateCall[3]).toBe(4) -- actual time
					jestExpect(updateCall[4]).toBe(17) -- base time
					jestExpect(updateCall[5]).toBe(58) -- start time
					jestExpect(updateCall[6]).toBe(62) -- commit time
				end
			)

			it("includes time spent in render phase lifecycles", function()
				local WithLifecycles = React.Component:extend("WithLifecycles")
				function WithLifecycles:init()
					self.state = {}
				end
				WithLifecycles.getDerivedStateFromProps = function()
					Scheduler.unstable_advanceTime(3)
					return nil
				end
				function WithLifecycles:shouldComponentUpdate()
					Scheduler.unstable_advanceTime(7)
					return true
				end
				function WithLifecycles:render()
					Scheduler.unstable_advanceTime(5)
					return nil
				end

				local callback = jest.fn()

				Scheduler.unstable_advanceTime(5) -- 0 -> 5

				local renderer = ReactTestRenderer.create(
					React.createElement(
						React.Profiler,
						{ id = "test", onRender = callback },
						React.createElement(WithLifecycles)
					)
				)

				Scheduler.unstable_advanceTime(15) -- 13 -> 28

				renderer.update(
					React.createElement(
						React.Profiler,
						{ id = "test", onRender = callback },
						React.createElement(WithLifecycles)
					)
				)

				jestExpect(callback).toHaveBeenCalledTimes(2)

				local mountCall, updateCall =
					callback.mock.calls[1], callback.mock.calls[2]

				jestExpect(mountCall[2]).toBe("mount")
				jestExpect(mountCall[3]).toBe(8) -- actual time
				jestExpect(mountCall[4]).toBe(8) -- base time
				jestExpect(mountCall[5]).toBe(5) -- start time
				jestExpect(mountCall[6]).toBe(13) -- commit time

				jestExpect(updateCall[2]).toBe("update")
				jestExpect(updateCall[3]).toBe(15) -- actual time
				jestExpect(updateCall[4]).toBe(15) -- base time
				jestExpect(updateCall[5]).toBe(28) -- start time
				jestExpect(updateCall[6]).toBe(43) -- commit time
			end)

			describe("with regard to interruptions", function()
				for _, replayFailedUnitOfWorkWithInvokeGuardedCallback in { true, false } do
					describe(
						"replayFailedUnitOfWorkWithInvokeGuardedCallback "
							.. (function()
								if replayFailedUnitOfWorkWithInvokeGuardedCallback then
									return "enabled"
								end
								return "disabled"
							end)(),
						function()
							beforeEach(function()
								jest.resetModules()

								loadModules({
									replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback,
								})
							end)

							it(
								"should accumulate actual time after an error handled by componentDidCatch()",
								function()
									local callback = jest.fn()

									local ThrowsError = function(props)
										local _unused = props.unused
										Scheduler.unstable_advanceTime(3)
										error("expected error")
									end

									local ErrorBoundary =
										React.Component:extend("ErrorBoundary")
									function ErrorBoundary:init()
										self.state = { error_ = nil }
									end
									function ErrorBoundary:componentDidCatch(error_)
										self:setState({ error_ = error_ })
									end
									function ErrorBoundary:render()
										Scheduler.unstable_advanceTime(2)
										return (function()
											if self.state.error_ == nil then
												return self.props.children
											end
											return React.createElement(
												AdvanceTime,
												{ byAmount = 20 }
											)
										end)()
									end

									Scheduler.unstable_advanceTime(5) -- 0 -> 5

									ReactTestRenderer.create(
										React.createElement(
											React.Profiler,
											{ id = "test", onRender = callback },
											React.createElement(
												ErrorBoundary,
												nil,
												React.createElement(
													AdvanceTime,
													{ byAmount = 9 }
												),
												React.createElement(ThrowsError)
											)
										)
									)

									jestExpect(callback).toHaveBeenCalledTimes(2)

									-- Callbacks bubble (reverse order).
									local mountCall, updateCall =
										callback.mock.calls[1], callback.mock.calls[2]

									-- The initial mount only includes the ErrorBoundary (which takes 2)
									-- But it spends time rendering all of the failed subtree also.
									jestExpect(mountCall[2]).toBe("mount")
									-- actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
									-- We don't count the time spent in replaying the failed unit of work (ThrowsError)
									jestExpect(mountCall[3]).toBe(14)
									-- base time includes: 2 (ErrorBoundary)
									-- Since the tree is empty for the initial commit
									jestExpect(mountCall[4]).toBe(2)
									-- start time
									jestExpect(mountCall[5]).toBe(5)
									-- commit time: 5 initially + 14 of work
									-- Add an additional 3 (ThrowsError) if we replayed the failed work
									jestExpect(mountCall[6]).toBe((function()
										if
											ReactGlobals.__DEV__
											and replayFailedUnitOfWorkWithInvokeGuardedCallback
										then
											return 22
										end
										return 19
									end)())

									-- The update includes the ErrorBoundary and its fallback child
									jestExpect(updateCall[2]).toBe("update")
									-- actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
									jestExpect(updateCall[3]).toBe(22)
									-- base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
									jestExpect(updateCall[4]).toBe(22)
									-- start time
									jestExpect(updateCall[5]).toBe((function()
										if
											ReactGlobals.__DEV__
											and replayFailedUnitOfWorkWithInvokeGuardedCallback
										then
											return 22
										end
										return 19
									end)())

									-- commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
									-- Add an additional 3 (ThrowsError) if we replayed the failed work
									jestExpect(updateCall[6]).toBe((function()
										if
											ReactGlobals.__DEV__
											and replayFailedUnitOfWorkWithInvokeGuardedCallback
										then
											return 44
										end
										return 41
									end)())
								end
							)

							it(
								"should accumulate actual time after an error handled by getDerivedStateFromError()",
								function()
									local callback = jest.fn()

									local ThrowsError = function(props)
										local _unused = props.unused
										Scheduler.unstable_advanceTime(10)
										error("expected error")
									end

									local ErrorBoundary =
										React.Component:extend("ErrorBoundary")
									function ErrorBoundary:init()
										self.state = { error_ = nil }
									end
									function ErrorBoundary.getDerivedStateFromError(
										error_
									)
										return { error_ = error_ }
									end
									function ErrorBoundary:render()
										Scheduler.unstable_advanceTime(2)
										return (function()
											if self.state.error_ == nil then
												return self.props.children
											end
											return React.createElement(
												AdvanceTime,
												{ byAmount = 20 }
											)
										end)()
									end

									Scheduler.unstable_advanceTime(5) -- 0 -> 5

									ReactTestRenderer.create(
										React.createElement(
											React.Profiler,
											{ id = "test", onRender = callback },
											React.createElement(
												ErrorBoundary,
												nil,
												React.createElement(
													AdvanceTime,
													{ byAmount = 5 }
												),
												React.createElement(ThrowsError)
											)
										)
									)

									jestExpect(callback).toHaveBeenCalledTimes(1)

									-- Callbacks bubble (reverse order).
									local mountCall = callback.mock.calls[1]

									-- The initial mount includes the ErrorBoundary's error state,
									-- But it also spends actual time rendering UI that fails and isn't included.
									jestExpect(mountCall[2]).toBe("mount")
									-- actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
									-- Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
									-- We don't count the time spent in replaying the failed unit of work (ThrowsError)
									jestExpect(mountCall[3]).toBe(39)
									-- base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
									jestExpect(mountCall[4]).toBe(22)
									-- start time
									jestExpect(mountCall[5]).toBe(5)
									-- commit time
									jestExpect(mountCall[6]).toBe((function()
										if
											ReactGlobals.__DEV__
											and replayFailedUnitOfWorkWithInvokeGuardedCallback
										then
											return 54
										end
										return 44
									end)())
								end
							)

							it(
								'should reset the fiber stack correct after a "complete" phase error',
								function()
									jest.resetModules()

									loadModules({
										useNoopRenderer = true,
										replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback,
									})

									-- Simulate a renderer error during the "complete" phase.
									-- This mimics behavior like React Native's View/Text nesting validation.
									ReactNoop.render(
										React.createElement(
											React.Profiler,
											{ id = "profiler", onRender = jest.fn() },
											React.createElement(
												"errorInCompletePhase",
												nil,
												"hi"
											)
										)
									)
									jestExpect(Scheduler).toFlushAndThrow(
										"Error in host config."
									)

									-- A similar case we've seen caused by an invariant in ReactDOM.
									-- It didn't reproduce without a host component inside.
									ReactNoop.render(
										React.createElement(
											React.Profiler,
											{ id = "profiler", onRender = jest.fn() },
											React.createElement(
												"errorInCompletePhase",
												nil,
												React.createElement("span", nil, "hi")
											)
										)
									)
									jestExpect(Scheduler).toFlushAndThrow(
										"Error in host config."
									)

									-- So long as the profiler timer's fiber stack is reset correctly,
									-- Subsequent renders should not error.
									ReactNoop.render(
										React.createElement(
											React.Profiler,
											{ id = "profiler", onRender = jest.fn() },
											React.createElement("span", nil, "hi")
										)
									)
									jestExpect(Scheduler).toFlushWithoutYielding()
								end
							)
						end
					)
				end
			end)
		end)
	end
end)
