--[[
	Represents a tree of tests that have been loaded but not necessarily
	executed yet.

	TestPlan objects are produced by TestPlanner.
]]

local TestEnum = require(script.Parent.TestEnum)
local Expectation = require(script.Parent.Expectation)

local function getCoreScriptSyncService()
	local success, result = pcall(function()
		return game:GetService("CoreScriptSyncService")
	end)

	return success and result or nil
end

local CoreScriptSyncService = nil

local function getFilePathFromInstance(instance)
	if CoreScriptSyncService == nil then
		CoreScriptSyncService = getCoreScriptSyncService() or false
	end

	local success, result = pcall(function()
		if not CoreScriptSyncService then
			return nil
		end
		return CoreScriptSyncService:GetScriptFilePath(instance)
	end)

	return (success and result or nil)
end

local function newEnvironment(currentNode, extraEnvironment, filePath)
	local env = {}

	if extraEnvironment then
		if type(extraEnvironment) ~= "table" then
			error(("Bad argument #2 to newEnvironment. Expected table, got %s"):format(typeof(extraEnvironment)), 2)
		end

		for key, value in pairs(extraEnvironment) do
			env[key] = value
		end
	end

	local function addChild(phrase, callback, nodeType, nodeModifier)
		local node = currentNode:addChild(phrase, nodeType, nodeModifier, filePath)
		node.callback = callback
		if nodeType == TestEnum.NodeType.Describe then
			node:expand()
		end
		return node
	end

	function env.describeFOCUS(phrase, callback)
		addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Focus)
	end

	function env.describeSKIP(phrase, callback)
		addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Skip)
	end

	function env.describe(phrase, callback, nodeModifier)
		addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.None)
	end

	function env.itFOCUS(phrase, callback)
		addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus)
	end

	function env.itSKIP(phrase, callback)
		addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
	end

	function env.itFIXME(phrase, callback)
		local node = addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
		warn("FIXME: broken test", node:getFullName())
	end

	function env.it(phrase, callback, nodeModifier)
		addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.None)
	end

	-- Incrementing counter used to ensure that beforeAll, afterAll, beforeEach, afterEach have unique phrases
	local lifecyclePhaseId = 0

	local lifecycleHooks = {
		[TestEnum.NodeType.BeforeAll] = "beforeAll",
		[TestEnum.NodeType.AfterAll] = "afterAll",
		[TestEnum.NodeType.BeforeEach] = "beforeEach",
		[TestEnum.NodeType.AfterEach] = "afterEach",
	}

	for nodeType, name in pairs(lifecycleHooks) do
		env[name] = function(callback)
			addChild(name .. "_" .. tostring(lifecyclePhaseId), callback, nodeType, TestEnum.NodeModifier.None)
			lifecyclePhaseId = lifecyclePhaseId + 1
		end
	end

	function env.FIXME(optionalMessage)
		warn("FIXME: broken test", currentNode:getFullName(), optionalMessage or "")

		currentNode.modifier = TestEnum.NodeModifier.Skip
	end

	function env.FOCUS()
		currentNode.modifier = TestEnum.NodeModifier.Focus
	end

	function env.SKIP()
		currentNode.modifier = TestEnum.NodeModifier.Skip
	end

	--[[
		This function is deprecated. Calling it is a no-op beyond generating a
		warning.
	]]
	function env.HACK_NO_XPCALL()
		warn(
			"HACK_NO_XPCALL is deprecated. It is now safe to yield in an "
				.. "xpcall, so this is no longer necessary. It can be safely deleted."
		)
	end

	env.fit = env.itFOCUS
	env.xit = env.itSKIP
	env.fdescribe = env.describeFOCUS
	env.xdescribe = env.describeSKIP

	env.expect = setmetatable({
		extend = function(...)
			error('Cannot call "expect.extend" from within a "describe" node.')
		end,
	}, {
		__call = function(_self, ...)
			return Expectation.new(...)
		end,
	})

	return env
end

local TestNode = {}
TestNode.__index = TestNode