--[[
	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 newEnvironment(currentNode, extraEnvironment)
	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)
		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