-- Upstream: https://github.com/facebook/react/blob/16654436039dd8f16a63928e71081c7745872e8f/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js
--!strict
-- /**
--  * Copyright (c) Facebook, Inc. and its affiliates.
--  *
--  * This source code is licensed under the MIT license found in the
--  * LICENSE file in the root directory of this source tree.
--  *
--  * @emails react-core
--  * @jest-environment node
--  */

local Packages = script.Parent.Parent.Parent

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

local LuauPolyfill = require(Packages.LuauPolyfill)
local Object = LuauPolyfill.Object

local React
local ReactTestRenderer
local Context
local RCTView = "RCTView"
local View

describe("ReactTestRendererTraversal", function()
	local ExampleFn
	local ExampleNull
	local ExampleSpread
	local ExampleForwardRef

	local Example

	beforeEach(function()
		jest.resetModules()
		React = require(Packages.React)
		View = function(props)
			return React.createElement(RCTView, props)
		end
		ReactTestRenderer = require(Packages.ReactTestRenderer)
		ExampleSpread = React.Component:extend("ExampleSpread")
		Context = React.createContext(nil)

		Example = React.Component:extend("Example")
		function Example:render()
			return React.createElement(
				View,
				nil,
				React.createElement(
					View,
					{
						foo = "foo",
					},
					React.createElement(View, {
						bar = "bar",
					}),
					React.createElement(View, {
						bar = "bar",
						baz = "baz",
						itself = "itself",
					}),
					React.createElement(View),
					React.createElement(ExampleSpread, {
						bar = "bar",
					}),
					React.createElement(ExampleFn, {
						bar = "bar",
						bing = "bing",
					}),
					React.createElement(ExampleNull, {
						bar = "bar",
					}),
					React.createElement(
						ExampleNull,
						{
							null = "null",
						},
						React.createElement(View, {
							void = "void",
						}),
						React.createElement(View, {
							void = "void",
						})
					),
					React.createElement(
						React.Profiler,
						{
							id = "test",
							onRender = function()
								return
							end,
						},
						React.createElement(ExampleForwardRef, {
							qux = "qux",
						})
					),
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(
							React.Fragment,
							nil,
							React.createElement(
								Context.Provider,
								{ value = Object.None },
								React.createElement(Context.Consumer, nil, function()
									return React.createElement(View, { nested = true })
								end)
							)
						),
						React.createElement(View, { nested = true }),
						React.createElement(View, { nested = true })
					)
				)
			)
		end

		function ExampleSpread:render()
			return React.createElement(View, self.props)
		end

		ExampleFn = function(props)
			return React.createElement(View, {
				baz = "baz",
			})
		end
		ExampleNull = function(props)
			return nil
		end
		ExampleForwardRef = React.forwardRef(function(props, ref)
			-- ROBLOX deviation: no easy spread operator, and tests don't demand a generic solution
			return React.createElement(View, { qux = props.qux, ref = ref })
		end)
	end)

	it("initializes", function()
		local render = ReactTestRenderer.create(React.createElement(Example))
		local hasFooProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "foo") ~= nil
		end

		-- assert .props, .type and .parent attributes
		local foo = render.root:find(hasFooProp)

		jestExpect(foo.props.children).toHaveLength(9)
		jestExpect(foo.type).toBe(View)
		jestExpect(render.root.parent).toBe(nil)
		-- ROBLOX FIXME: when this matches upstream, jestExpect takes an hour to complete
		jestExpect(foo.children[1].parent._fiber).toBe(foo._fiber)
	end)
	it("searches via .find() / .findAll()", function()
		local render = ReactTestRenderer.create(React.createElement(Example))
		local hasFooProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "foo") ~= nil
		end
		local hasBarProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "bar") ~= nil
		end
		local hasBazProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "baz") ~= nil
		end
		local hasBingProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "bing") ~= nil
		end
		local hasNullProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "null") ~= nil
		end
		local hasVoidProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "void") ~= nil
		end
		local hasItselfProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "itself") ~= nil
		end
		local hasNestedProp = function(node)
			-- ROBLOX deviation: workaround for hasOwnProperty
			return rawget(node.props, "nested") ~= nil
		end

		jestExpect(function()
			return render.root:find(hasFooProp)
		end).never.toThrow() -- 1 match
		jestExpect(function()
			return render.root:find(hasBarProp)
		end).toThrow() -- >1 matches
		jestExpect(function()
			return render.root:find(hasBazProp)
		end).toThrow() -- >1 matches
		jestExpect(function()
			return render.root:find(hasBingProp)
		end).never.toThrow() -- 1 match
		jestExpect(function()
			return render.root:find(hasNullProp)
		end).never.toThrow() -- 1 match
		jestExpect(function()
			return render.root:find(hasVoidProp)
		end).toThrow() -- 0 matches
		jestExpect(function()
			return render.root:find(hasNestedProp)
		end).toThrow() -- >1 matches

		-- same assertion as :find(), but confirm length
		jestExpect(render.root:findAll(hasFooProp, { deep = false })).toHaveLength(1)
		jestExpect(render.root:findAll(hasBarProp, { deep = false })).toHaveLength(5)
		jestExpect(render.root:findAll(hasBazProp, { deep = false })).toHaveLength(2)
		jestExpect(render.root:findAll(hasBingProp, { deep = false })).toHaveLength(1)
		jestExpect(render.root:findAll(hasNullProp, { deep = false })).toHaveLength(1)
		jestExpect(render.root:findAll(hasVoidProp, { deep = false })).toHaveLength(0)
		jestExpect(render.root:findAll(hasNestedProp, { deep = false })).toHaveLength(3)

		-- note: with {deep: true}, :findAll() will continue to
		--       search children, even after finding a match
		jestExpect(render.root:findAll(hasFooProp)).toHaveLength(2)
		jestExpect(render.root:findAll(hasBarProp)).toHaveLength(9)
		jestExpect(render.root:findAll(hasBazProp)).toHaveLength(4)
		jestExpect(render.root:findAll(hasBingProp)).toHaveLength(1) -- no spread
		jestExpect(render.root:findAll(hasNullProp)).toHaveLength(1) -- no spread
		jestExpect(render.root:findAll(hasVoidProp)).toHaveLength(0)
		jestExpect(render.root:findAll(hasNestedProp, { deep = false })).toHaveLength(3)

		local bing = render.root:find(hasBingProp)

		jestExpect(bing:find(hasBarProp)).toBe(bing)
		jestExpect(bing:find(hasBingProp)).toBe(bing)
		jestExpect(bing:findAll(hasBazProp, { deep = false })).toHaveLength(1)
		jestExpect(bing:findAll(hasBazProp)).toHaveLength(2)

		local foo = render.root:find(hasFooProp)
		jestExpect(foo:findAll(hasFooProp, { deep = false })).toHaveLength(1)
		jestExpect(foo:findAll(hasFooProp)).toHaveLength(2)

		local itself = foo:find(hasItselfProp)

		jestExpect(itself:find(hasBarProp)).toBe(itself)
		jestExpect(itself:find(hasBazProp)).toBe(itself)
		jestExpect(itself:findAll(hasBazProp, { deep = false })).toHaveLength(1)
		jestExpect(itself:findAll(hasBazProp)).toHaveLength(2)
	end)
	it("searches via .findByType() / .findAllByType()", function()
		local render = ReactTestRenderer.create(React.createElement(Example))
		jestExpect(function()
			return render.root:findByType(ExampleFn)
		end).never.toThrow() -- 1 match
		jestExpect(function()
			return render.root:findByType(View)
		end).never.toThrow() -- 1 match

		jestExpect(function()
			return render.root:findByType(ExampleForwardRef)
		end).never.toThrow() -- 1 match

		-- note: there are clearly multiple <View /> in general, but there
		--       is only one being rendered at root node level
		jestExpect(function()
			return render.root:findByType(ExampleNull)
		end).toThrow() -- 2 matches
		jestExpect(#render.root:findAllByType(ExampleFn)).toEqual(1)
		jestExpect(#render.root:findAllByType(View, { deep = false })).toEqual(1)

		jestExpect(#render.root:findAllByType(View)).toEqual(11)

		jestExpect(#render.root:findAllByType(ExampleNull)).toEqual(2)

		jestExpect(#render.root:findAllByType(ExampleForwardRef)).toEqual(1)

		local nulls = render.root:findAllByType(ExampleNull)

		jestExpect(#nulls[1]:findAllByType(View)).toEqual(0)
		jestExpect(#nulls[2]:findAllByType(View)).toEqual(0)

		local fn = render.root:findAllByType(ExampleFn)

		jestExpect(#fn[1]:findAllByType(View)).toEqual(1)
	end)
	it("searches via .findByProps() / .findAllByProps()", function()
		local render = ReactTestRenderer.create(React.createElement(Example))
		local foo = "foo"
		local bar = "bar"
		local baz = "baz"
		local qux = "qux"

		jestExpect(function()
			return render.root:findByProps({ foo = foo })
		end).never.toThrow() -- 1 match
		jestExpect(function()
			return render.root:findByProps({ bar = bar })
		end).toThrow() -- >1 matches
		jestExpect(function()
			return render.root:findByProps({ baz = baz })
		end).toThrow() -- >1 matches

		jestExpect(function()
			return render.root:findByProps({ qux = qux })
		end).never.toThrow() -- 1 match

		jestExpect(render.root:findAllByProps({ foo = foo }, { deep = false })).toHaveLength(
			1
		)
		jestExpect(render.root:findAllByProps({ bar = bar }, { deep = false })).toHaveLength(
			5
		)
		jestExpect(render.root:findAllByProps({ baz = baz }, { deep = false })).toHaveLength(
			2
		)
		jestExpect(render.root:findAllByProps({ qux = qux }, { deep = false })).toHaveLength(
			1
		)

		jestExpect(render.root:findAllByProps({ foo = foo })).toHaveLength(2)
		jestExpect(render.root:findAllByProps({ bar = bar })).toHaveLength(9)
		jestExpect(render.root:findAllByProps({ baz = baz })).toHaveLength(4)
		-- ROBLOX FIXME: this assert currently fails, only gets 1
		jestExpect(render.root:findAllByProps({ qux = qux })).toHaveLength(3)
	end)
	it("skips special nodes", function()
		local render = ReactTestRenderer.create(React.createElement(Example))

		jestExpect(render.root:findAllByType(React.Fragment)).toHaveLength(0)
		jestExpect(render.root:findAllByType(Context.Consumer)).toHaveLength(0)
		jestExpect(render.root:findAllByType(Context.Provider)).toHaveLength(0)

		local expectedParent = render.root:findByProps({
			foo = "foo",
		}, {
			deep = false,
		}).children[1]
		local nestedViews = render.root:findAllByProps(
			{ nested = true },
			{ deep = false }
		)

		jestExpect(#nestedViews).toEqual(3)

		-- ROBLOX FIXME: jest expect take a long time to complete, it needs to be fixed
		jestExpect(nestedViews[1].parent._fiber).toBe(expectedParent._fiber)
		jestExpect(nestedViews[2].parent._fiber).toBe(expectedParent._fiber)
		jestExpect(nestedViews[3].parent._fiber).toBe(expectedParent._fiber)
	end)
	it("can have special nodes as roots", function()
		local FR = React.forwardRef(function(props, ref)
			return React.createElement("section", props)
		end)

		jestExpect(
			#ReactTestRenderer.create(
				React.createElement(
					FR,
					nil,
					React.createElement("div"),
					React.createElement("div")
				)
			).root
				:findAllByType("div")
		).toEqual(2)
		jestExpect(
			#ReactTestRenderer.create(
				React.createElement(
					React.Fragment,
					nil,
					React.createElement("div"),
					React.createElement("div")
				)
			).root
				:findAllByType("div")
		).toEqual(2)
		jestExpect(#ReactTestRenderer.create(React.createElement(React.Fragment, {
			key = "foo",
		}, React.createElement("div", nil), React.createElement("div", nil))).root
			:findAllByType("div")).toEqual(2)
		jestExpect(
			#ReactTestRenderer.create(
				React.createElement(
					React.StrictMode,
					nil,
					React.createElement("div"),
					React.createElement("div")
				)
			).root
				:findAllByType("div")
		).toEqual(2)
		jestExpect(
			#ReactTestRenderer.create(
				React.createElement(
					Context.Provider,
					{ value = Object.None },
					React.createElement("div"),
					React.createElement("div")
				)
			).root
				:findAllByType("div")
		).toEqual(2)
	end)
end)
