--[[
	* 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 Packages = script.Parent.Parent.Parent.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
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(Packages.Shared).ReactFeatureFlags
	ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false

	React = require(Packages.React)
	ReactRoblox = require(Packages.ReactRoblox)
	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)
