local j=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\106\101\115\116-\103\108\111\098\097\108\115")local d=j.expect local A=j.jest local f=j.beforeEach local T=j.it local Z=j.describe local O local I local g local G f(function()A.resetModules()A.useFakeTimers()local j=(require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\115\104\097\114\101\100")).ReactFeatureFlags j.debugRenderPhaseSideEffectsForStrictMode=false O=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\114\101\097\099\116")I=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\114\101\097\099\116-\114\111\098\108\111\120")G=Instance.new("\070\111\108\100\101\114")g=I.createRoot(G)end)T("\115\104\111\117\108\100 \117\112\100\097\116\101 \097 \118\097\108\117\101 \119\105\116\104\111\117\116 \114\101-\114\101\110\100\101\114\105\110\103",function()local j,A=O.createBinding("\104\101\108\108\111")local f=0 local function T(d)f+=1 return O.createElement("\084\101\120\116\076\097\098\101\108",{Name="\076\097\098\101\108";Text=j})end I.act(function()g:render(O.createElement(T))end);(d(f)).toBe(1);(d(G.Label.Text)).toBe("\104\101\108\108\111")A("\119\111\114\108\100");(d(f)).toBe(1);(d(G.Label.Text)).toBe("\119\111\114\108\100")end)T("\115\117\098\115\099\114\105\098\101 \116\111 \117\112\100\097\116\101\115 \119\104\101\110 \117\115\101\100 \097\115 \097 \114\101\102",function()local j=O.createRef()local A=O.createRef()local function f(d)return O.createElement(O.Fragment,nil,{Left=O.createElement("\084\101\120\116\066\117\116\116\111\110",{ref=j;NextSelectionRight=A});Right=O.createElement("\084\101\120\116\066\117\116\116\111\110",{ref=A,NextSelectionRight=j})})end I.act(function()g:render(O.createElement(f))end);(d(j.current)).never.toBeNil();(d(A.current)).never.toBeNil();(d(j.current.NextSelectionRight)).toBe(A.current);(d(A.current.NextSelectionRight)).toBe(j.current)end)T("\115\104\111\117\108\100 \110\111\116 \114\101\116\117\114\110 \116\104\101 \115\097\109\101 \114\111\111\116 \116\119\105\099\101",function()local j=Instance.new("\070\111\108\100\101\114")local A=I.createRoot(j);(d(g)).never.toBe(A)end)T("\115\104\111\117\108\100 \117\110\115\117\098\115\099\114\105\098\101 \102\114\111\109 \098\105\110\100\105\110\103\115 \119\104\101\110 \117\110\109\111\117\110\116\101\100",function()local j,A=O.createBinding(0)local f=0 local function T()return O.createElement("\084\101\120\116\076\097\098\101\108",{Text=j:map(function(j)f+=1 return tostring(j)end)})end I.act(function()g:render(O.createElement(T))end);(d(f)).toBe(1)I.act(function()g:render(O.createElement(T))end);(d(f)).toBe(2)A(1);(d(f)).toBe(3)I.act(function()g:unmount()end)A(2);(d(f)).toBe(3)end)Z("\117\115\101\066\105\110\100\105\110\103 \104\111\111\107",function()T("\114\101\116\117\114\110\115 \116\104\101 \115\097\109\101 \098\105\110\100\105\110\103 \111\098\106\101\099\116 \101\097\099\104 \116\105\109\101",function()local j=A.fn()local f local function T(d)local A,T=O.useBinding("\104\101\108\108\111")local Z,I=O.useState(1)j(A,T)f=function()I(function(j)return j+1 end)end return O.createElement("\084\101\120\116\076\097\098\101\108",{Name="\076\097\098\101\108";LayoutOrder=Z,Text=A})end I.act(function()g:render(O.createElement(T))end);(d(j)).toHaveBeenCalledTimes(1);(d(G.Label.Text)).toBe("\104\101\108\108\111");(d(G.Label.LayoutOrder)).toBe(1)I.act(function()f()end);(d(j)).toHaveBeenCalledTimes(2)local Z=j.mock.calls;(d(Z[1])).toEqual(Z[2]);(d(G.Label.Text)).toBe("\104\101\108\108\111");(d(G.Label.LayoutOrder)).toBe(2)end)T("\117\112\100\097\116\101\115 \116\104\101 \114\101\108\101\118\097\110\116 \112\114\111\112\101\114\116\121 \119\105\116\104\111\117\116 \114\101-\114\101\110\100\101\114\105\110\103",function()local j local A=0 local function f(d)local f,T=O.useBinding("\104\101\108\108\111")j=T A+=1 return O.createElement("\084\101\120\116\076\097\098\101\108",{Name="\076\097\098\101\108",Text=f})end I.act(function()g:render(O.createElement(f))end);(d(A)).toBe(1);(d(G.Label.Text)).toBe("\104\101\108\108\111")j("\119\111\114\108\100");(d(A)).toBe(1);(d(G.Label.Text)).toBe("\119\111\114\108\100")end)T("\099\097\110 \098\101 \117\115\101\100 \119\105\116\104 \109\097\112\112\101\100 \098\105\110\100\105\110\103\115",function()local j local A=0 local function f(d)local f,T=O.useBinding("\104\101\108\108\111")j=T A+=1 return O.createElement("\084\101\120\116\076\097\098\101\108",{Name="\076\097\098\101\108";Text=f:map(function(j)return string.reverse(j)end)})end I.act(function()g:render(O.createElement(f))end);(d(A)).toBe(1);(d(G.Label.Text)).toBe("\111\108\108\101\104")j("\119\111\114\108\100");(d(A)).toBe(1);(d(G.Label.Text)).toBe("\100\108\114\111\119")end)T("\109\097\112\112\101\100 \098\105\110\100\105\110\103\115 \099\097\110 \098\101 \114\101-\109\097\112\112\101\100",function()local function j(j)return O.createElement("\084\101\120\116\076\097\098\101\108",{Name="\076\097\098\101\108\076\101\110\103\116\104";Text=j.length:map(function(j)return"\076\101\110\103\116\104\058 "..tostring(j)end)})end local A local f=0 local function T(d)local T,Z=O.useBinding("\104\101\108\108\111")A=Z f+=1 return O.createElement(O.Fragment,nil,O.createElement("\084\101\120\116\076\097\098\101\108",{Name="\076\097\098\101\108",Text=T:map(string.reverse)}),O.createElement(j,{length=T:map(string.len)}))end I.act(function()g:render(O.createElement(T))end);(d(f)).toBe(1);(d(G.Label.Text)).toBe("\111\108\108\101\104");(d(G.LabelLength.Text)).toBe("\076\101\110\103\116\104\058 \053")A("\102\114\105\101\110\100\115");(d(f)).toBe(1);(d(G.Label.Text)).toBe("\115\100\110\101\105\114\102");(d(G.LabelLength.Text)).toBe("\076\101\110\103\116\104\058 \055")end)T("\099\097\110 \098\101 \117\115\101\100 \119\105\116\104 \106\111\105\110\101\100 \098\105\110\100\105\110\103\115",function()local j,A local f=0 local function T(d)local T,Z=O.useBinding("\071\114\101\101\116\105\110\103\058")local I,g=O.useBinding("\104\101\108\108\111")j,A=Z,g f+=1 local G=O.joinBindings({T;I})return O.createElement("\084\101\120\116\076\097\098\101\108",{Name="\076\097\098\101\108";Text=G:map(function(j)return table.concat(j," ")end)})end I.act(function()g:render(O.createElement(T))end);(d(f)).toBe(1);(d(G.Label.Text)).toBe("\071\114\101\101\116\105\110\103\058 \104\101\108\108\111")j("\083\097\108\117\116\097\116\105\111\110\058");(d(f)).toBe(1);(d(G.Label.Text)).toBe("\083\097\108\117\116\097\116\105\111\110\058 \104\101\108\108\111")A("\115\117\112");(d(f)).toBe(1);(d(G.Label.Text)).toBe("\083\097\108\117\116\097\116\105\111\110\058 \115\117\112")end)end)
--[[
	* Copyright (c) Roblox Corporation. All rights reserved.
	* Licensed under the MIT License (the "License");
	* you may not use this file except in compliance with the License.
	* You may obtain a copy of the License at
	*
	*     https://opensource.org/licenses/MIT
	*
	* Unless required by applicable law or agreed to in writing, software
	* distributed under the License is distributed on an "AS IS" BASIS,
	* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	* See the License for the specific language governing permissions and
	* limitations under the License.
]]

local JestGlobals = require("@pkg/@jsdotlua/jest-globals")
local jestExpect = JestGlobals.expect
local jest = JestGlobals.jest
local beforeEach = JestGlobals.beforeEach
local it = JestGlobals.it
local describe = JestGlobals.describe

local React
local ReactRoblox
local reactRobloxRoot
local parent

beforeEach(function()
	jest.resetModules()
	jest.useFakeTimers()
	local ReactFeatureFlags = require("@pkg/@jsdotlua/shared").ReactFeatureFlags
	ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false

	React = require("@pkg/@jsdotlua/react")
	ReactRoblox = require("@pkg/@jsdotlua/react-roblox")
	parent = Instance.new("Folder")
	reactRobloxRoot = ReactRoblox.createRoot(parent)
end)

it("should update a value without re-rendering", function()
	local value, setValue = React.createBinding("hello")
	local renderCount = 0
	local function Component(props)
		renderCount += 1
		return React.createElement("TextLabel", {
			Name = "Label",
			Text = value,
		})
	end

	ReactRoblox.act(function()
		reactRobloxRoot:render(React.createElement(Component))
	end)

	jestExpect(renderCount).toBe(1)
	jestExpect(parent.Label.Text).toBe("hello")

	setValue("world")

	jestExpect(renderCount).toBe(1)
	jestExpect(parent.Label.Text).toBe("world")
end)

it("subscribe to updates when used as a ref", function()
	local leftButtonRef = React.createRef()
	local rightButtonRef = React.createRef()

	local function Component(props)
		return React.createElement(React.Fragment, nil, {
			Left = React.createElement("TextButton", {
				ref = leftButtonRef,
				NextSelectionRight = rightButtonRef,
			}),
			Right = React.createElement("TextButton", {
				ref = rightButtonRef,
				NextSelectionRight = leftButtonRef,
			}),
		})
	end

	ReactRoblox.act(function()
		reactRobloxRoot:render(React.createElement(Component))
	end)

	jestExpect(leftButtonRef.current).never.toBeNil()
	jestExpect(rightButtonRef.current).never.toBeNil()

	jestExpect(leftButtonRef.current.NextSelectionRight).toBe(rightButtonRef.current)
	jestExpect(rightButtonRef.current.NextSelectionRight).toBe(leftButtonRef.current)
end)

it("should not return the same root twice", function()
	local parent2 = Instance.new("Folder")
	local reactRobloxRoot2 = ReactRoblox.createRoot(parent2)

	jestExpect(reactRobloxRoot).never.toBe(reactRobloxRoot2)
end)

it("should unsubscribe from bindings when unmounted", function()
	local counterBinding, setCounter = React.createBinding(0)
	local renderCount = 0
	local function Component()
		return React.createElement("TextLabel", {
			Text = counterBinding:map(function(counter)
				renderCount += 1
				return tostring(counter)
			end),
		})
	end
	ReactRoblox.act(function()
		reactRobloxRoot:render(React.createElement(Component))
	end)
	jestExpect(renderCount).toBe(1)
	ReactRoblox.act(function()
		reactRobloxRoot:render(React.createElement(Component))
	end)
	jestExpect(renderCount).toBe(2)
	setCounter(1)
	jestExpect(renderCount).toBe(3)
	ReactRoblox.act(function()
		reactRobloxRoot:unmount()
	end)
	setCounter(2)
	jestExpect(renderCount).toBe(3)
end)

describe("useBinding hook", function()
	it("returns the same binding object each time", function()
		local captureBinding = jest.fn()
		local updateComponent
		local function Component(props)
			local binding, updater = React.useBinding("hello")
			local stateValue, updateStateValue = React.useState(1)
			captureBinding(binding, updater)
			updateComponent = function()
				updateStateValue(function(prev)
					return prev + 1
				end)
			end

			return React.createElement("TextLabel", {
				Name = "Label",
				LayoutOrder = stateValue,
				Text = binding,
			})
		end

		ReactRoblox.act(function()
			reactRobloxRoot:render(React.createElement(Component))
		end)

		jestExpect(captureBinding).toHaveBeenCalledTimes(1)
		jestExpect(parent.Label.Text).toBe("hello")
		jestExpect(parent.Label.LayoutOrder).toBe(1)

		ReactRoblox.act(function()
			updateComponent()
		end)

		jestExpect(captureBinding).toHaveBeenCalledTimes(2)
		local capturedBindings = captureBinding.mock.calls
		jestExpect(capturedBindings[1]).toEqual(capturedBindings[2])
		jestExpect(parent.Label.Text).toBe("hello")
		jestExpect(parent.Label.LayoutOrder).toBe(2)
	end)

	it("updates the relevant property without re-rendering", function()
		local updateBinding
		local renderCount = 0
		local function Component(props)
			local value, setValue = React.useBinding("hello")
			updateBinding = setValue
			renderCount += 1
			return React.createElement("TextLabel", {
				Name = "Label",
				Text = value,
			})
		end

		ReactRoblox.act(function()
			reactRobloxRoot:render(React.createElement(Component))
		end)

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("hello")

		updateBinding("world")

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("world")
	end)

	it("can be used with mapped bindings", function()
		local updateBinding
		local renderCount = 0
		local function Component(props)
			local text, setText = React.useBinding("hello")
			updateBinding = setText
			renderCount += 1
			return React.createElement("TextLabel", {
				Name = "Label",
				Text = text:map(function(value)
					return string.reverse(value)
				end),
			})
		end

		ReactRoblox.act(function()
			reactRobloxRoot:render(React.createElement(Component))
		end)

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("olleh")

		updateBinding("world")

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("dlrow")
	end)

	it("mapped bindings can be re-mapped", function()
		-- Accepts a binding and remaps it
		local function Remap(props)
			return React.createElement("TextLabel", {
				Name = "LabelLength",
				Text = props.length:map(function(value)
					return "Length: " .. tostring(value)
				end),
			})
		end

		local updateBinding
		local renderCount = 0
		local function Component(props)
			local text, setText = React.useBinding("hello")
			updateBinding = setText
			renderCount += 1
			return React.createElement(
				React.Fragment,
				nil,
				React.createElement("TextLabel", {
					Name = "Label",
					Text = text:map(string.reverse),
				}),
				React.createElement(Remap, {
					length = text:map(string.len),
				})
			)
		end

		ReactRoblox.act(function()
			reactRobloxRoot:render(React.createElement(Component))
		end)

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("olleh")
		jestExpect(parent.LabelLength.Text).toBe("Length: 5")

		updateBinding("friends")

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("sdneirf")
		jestExpect(parent.LabelLength.Text).toBe("Length: 7")
	end)

	it("can be used with joined bindings", function()
		local updatePrefix, updateText
		local renderCount = 0
		local function Component(props)
			local prefix, setPrefix = React.useBinding("Greeting:")
			local text, setText = React.useBinding("hello")
			updatePrefix, updateText = setPrefix, setText
			renderCount += 1

			local fullText = React.joinBindings({ prefix, text })

			return React.createElement("TextLabel", {
				Name = "Label",
				Text = fullText:map(function(values)
					return table.concat(values, " ")
				end),
			})
		end

		ReactRoblox.act(function()
			reactRobloxRoot:render(React.createElement(Component))
		end)

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("Greeting: hello")

		updatePrefix("Salutation:")

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("Salutation: hello")

		updateText("sup")

		jestExpect(renderCount).toBe(1)
		jestExpect(parent.Label.Text).toBe("Salutation: sup")
	end)
end)
