--[[
	* 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

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 Roact
local RoactCompat

local UNSUPPORTED = {
	-- Container for features that are unstable in current Roact
	UNSTABLE = true,
	-- Very old aliases for the top-level Roact tree management API
	reify = true,
	reconcile = true,
	teardown = true,
}
beforeEach(function()
	jest.resetModules()
	Roact = require(Packages.Dev.Roact)
	RoactCompat = require(script.Parent.Parent)
end)

it("has all interface members that old Roact exposes", function()
	for k, v in Roact do
		if UNSUPPORTED[k] then
			-- Skip any API members that are well and truly unsupported
			continue
		end

		jestExpect(RoactCompat).toHaveProperty(k)
		local memberType = typeof(v)
		-- Exports common Roact symbol keys as the reserved key names used
		-- by RoactCompat ("ref" and "children", respectively)
		if k == "Ref" or k == "Children" then
			memberType = "string"
		end
		-- Roact.Portal is replaced by ReactRoblox.createPortal. The
		-- latter is a function that returns a portal object, while the
		-- former is a special component type. By implementing Roact.Portal
		-- as a function component that returns `createPortal`'s result, we
		-- can get similar behavior, but with `Roact.Portal` having a
		-- different type.
		if k == "Portal" then
			memberType = "function"
		end
		jestExpect(typeof(RoactCompat[k])).toBe(memberType)
	end
end)

describe("warns about deprecated Roact API features", function()
	it("warns about createFragment", function()
		jestExpect(function()
			RoactCompat.createFragment({ div = RoactCompat.createElement("div") })
		end).toWarnDev(
			"Warning: The legacy Roact API 'createFragment' is deprecated",
			{ withoutStack = true }
		)
	end)

	it("warns about Component:extend() with no args", function()
		jestExpect(function()
			RoactCompat.Component:extend()
		end).toWarnDev(
			"Component:extend() accepting no arguments is deprecated",
			{ withoutStack = true }
		)
	end)

	it("warns about oneChild", function()
		jestExpect(function()
			RoactCompat.oneChild({ RoactCompat.createElement("div") })
		end).toWarnDev(
			"Warning: The legacy Roact API 'oneChild' is deprecated",
			{ withoutStack = true }
		)
	end)

	it("warns about setGlobalConfig", function()
		jestExpect(function()
			RoactCompat.setGlobalConfig({ propValidation = true })
		end).toWarnDev(
			"Warning: The legacy Roact API 'setGlobalConfig' is deprecated",
			{ withoutStack = true }
		)
	end)

	it("warns about Roact.Portal", function()
		local ReactRoblox = require(Packages.ReactRoblox)
		local target = Instance.new("Folder")
		local function withPortal(_props)
			return RoactCompat.createElement(RoactCompat.Portal, {
				target = target,
			}, RoactCompat.createElement("Frame"))
		end
		jestExpect(function()
			local root = ReactRoblox.createLegacyRoot(Instance.new("ScreenGui"))
			root:render(RoactCompat.createElement(withPortal))
		end).toWarnDev("Warning: The legacy Roact API 'Roact.Portal' is deprecated")
	end)

	it("warns about mount", function()
		jestExpect(function()
			RoactCompat.mount(RoactCompat.createElement("TextLabel", { Text = "Foo" }))
		end).toWarnDev({
			"Warning: The legacy Roact API 'mount' is deprecated",
		}, { withoutStack = true })
	end)

	it("warns about mount with invalid instance", function()
		jestExpect(function()
			jestExpect(function()
				RoactCompat.mount(
					RoactCompat.createElement("TextLabel", { Text = "Foo" }),
					"I'm not an instance!"
				)
			end).toWarnDev({
				"Warning: The legacy Roact API 'mount' is deprecated",
			}, { withoutStack = true })
		end).toThrow(
			"Cannot mount element (`TextLabel`) into a parent that is not a Roblox Instance (got type `string`)",
			{ withoutStack = true }
		)

		jestExpect(function()
			jestExpect(function()
				RoactCompat.mount(
					RoactCompat.createElement("Frame"),
					{ bogusParent = true }
				)
			end).toWarnDev({
				"Warning: The legacy Roact API 'mount' is deprecated",
			}, { withoutStack = true })
		end).toThrow(
			"Cannot mount element (`Frame`) into a parent that is not a Roblox Instance (got type `table`) \n{ bogusParent",
			{ withoutStack = true }
		)
	end)

	it("warns about update", function()
		local tree
		jestExpect(function()
			tree = RoactCompat.mount(
				RoactCompat.createElement("TextLabel", { Text = "Foo" })
			)
		end).toWarnDev(
			"Warning: The legacy Roact API 'mount' is deprecated",
			{ withoutStack = true }
		)

		jestExpect(function()
			RoactCompat.update(
				tree,
				RoactCompat.createElement("TextLabel", { Text = "Bar" })
			)
		end).toWarnDev(
			"Warning: The legacy Roact API 'update' is deprecated",
			{ withoutStack = true }
		)
	end)

	it("warns about unmount", function()
		local tree
		jestExpect(function()
			tree = RoactCompat.mount(
				RoactCompat.createElement("TextLabel", { Text = "Foo" })
			)
		end).toWarnDev(
			"Warning: The legacy Roact API 'mount' is deprecated",
			{ withoutStack = true }
		)

		jestExpect(function()
			RoactCompat.unmount(tree)
		end).toWarnDev(
			"Warning: The legacy Roact API 'unmount' is deprecated",
			{ withoutStack = true }
		)
	end)
end)

describe("handles uninitialized state", function()
	it("errors if uninitialized state is assigned", function()
		local ReactRoblox = require(Packages.ReactRoblox)
		local Scheduler = require(Packages.Dev.Scheduler)
		local parent = Instance.new("Folder")
		local Component = RoactCompat.Component:extend("Component")

		function Component:render()
			self.state.foo = "bar"
		end

		local componentInstance = RoactCompat.createElement(Component)
		local root = ReactRoblox.createRoot(parent)

		jestExpect(function()
			root:render(componentInstance)
			Scheduler.unstable_flushAllWithoutAsserting()
		end).toErrorDev(
			"Attempted to directly mutate state. Use setState to assign new values to state."
		)
	end)

	it("warns if uninitialized state is accessed", function()
		local ReactRoblox = require(Packages.ReactRoblox)
		local parent = Instance.new("Folder")
		local Scheduler = require(Packages.Dev.Scheduler)
		local Component = RoactCompat.Component:extend("Component")

		local capturedBool = false

		function Component:render()
			if self.state.foo == nil then
				capturedBool = true
			end
		end

		local componentInstance = RoactCompat.createElement(Component)
		local root = ReactRoblox.createRoot(parent)

		jestExpect(function()
			root:render(componentInstance)
			Scheduler.unstable_flushAllWithoutAsserting()
		end).toWarnDev(
			"Attempted to access uninitialized state. Use setState to initialize state"
		)

		jestExpect(capturedBool).toBe(true)
	end)
end)

describe("ChildArray Keys", function()
	it("Shozuld assign keys to children in an array", function()
		local ReactRoblox = require(Packages.ReactRoblox)
		local parent = Instance.new("Folder")
		local Scheduler = require(Packages.Dev.Scheduler)
		local Component = RoactCompat.Component:extend("Component")

		function Component:render()
			return RoactCompat.createElement("Frame", {}, {
				RoactCompat.createElement("TextLabel", { Text = "one" }),
				RoactCompat.createElement("TextLabel", { Text = "two" }),
				RoactCompat.createElement("TextLabel", { Text = "three" }),
			})
		end

		local componentInstance = RoactCompat.createElement(Component)

		local root = ReactRoblox.createRoot(parent)

		-- We expect this to warn us about the implicit keys even though
		-- it's assigning them to maintain ordering
		jestExpect(function()
			root:render(componentInstance)
			Scheduler.unstable_flushAllWithoutAsserting()
		end).toErrorDev(
			'Warning: Each child in a list should have a unique "key" prop.'
		)

		local firstChild = parent:FindFirstChild(1, true)
		jestExpect(firstChild).toBeDefined()
		jestExpect(firstChild.Text).toEqual("one")

		local secondChild = parent:FindFirstChild(2, true)
		jestExpect(secondChild).toBeDefined()
		jestExpect(secondChild.Text).toEqual("two")

		local thirdChild = parent:FindFirstChild(3, true)
		jestExpect(thirdChild).toBeDefined()
		jestExpect(thirdChild.Text).toEqual("three")
	end)

	it("Should not assign keys to children tables", function()
		local ReactRoblox = require(Packages.ReactRoblox)
		local Scheduler = require(Packages.Dev.Scheduler)
		local React = require(Packages.React)
		local parent = Instance.new("Folder")

		local function Component()
			local _, setState = React.useState(false)

			React.useEffect(function()
				setState(true)
			end, {})

			return React.createElement("Frame", nil, false, {})
		end

		local componentInstance = RoactCompat.createElement(Component)

		local root = ReactRoblox.createRoot(parent)

		root:render(componentInstance)
		Scheduler.unstable_flushAllWithoutAsserting()
	end)
end)
