-- If we write more than one character at a time it is possible that
-- Node.js exits in the middle of printing the result. This was first observed
-- in Node.js 0.10 and still persists in Node.js 6.7+.
-- Let's print the test failure summary character by character which is safer
-- when hundreds of tests are failing.
function SummaryReporter:_write(string_: string)
	local i = 1
	local strLen = utf8.len(string_)
	assert(strLen ~= nil)
	while i < strLen do
		self._process.stderr:write(String.charCodeAt(string_, i))
		i += 1
	end
end

function SummaryReporter:onRunStart(aggregatedResults: AggregatedResult, options: ReporterOnStartOptions)
	BaseReporter.onRunStart(self, aggregatedResults, options)
	self._estimatedTime = options.estimatedTime
end

function SummaryReporter:onRunComplete(contexts: Set<Context>, aggregatedResults: AggregatedResult)
	local numTotalTestSuites, testResults, wasInterrupted =
		aggregatedResults.numTotalTestSuites, aggregatedResults.testResults, aggregatedResults.wasInterrupted
	if Boolean.toJSBoolean(numTotalTestSuites) then
		local lastResult = testResults[#testResults]
		-- Print a newline if the last test did not fail to line up newlines
		-- similar to when an error would have been thrown in the test.
		if
			Boolean.toJSBoolean(self._globalConfig.verbose)
			and Boolean.toJSBoolean(lastResult)
			and not Boolean.toJSBoolean(lastResult.numFailingTests)
			and not Boolean.toJSBoolean(lastResult.testExecError)
		then
			self:log("")
		end
		self:_printSummary(aggregatedResults, self._globalConfig)
		self:_printSnapshotSummary(aggregatedResults.snapshot, self._globalConfig)
		if Boolean.toJSBoolean(numTotalTestSuites) then
			local message = getSummary(aggregatedResults, { estimatedTime = self._estimatedTime })
			if not Boolean.toJSBoolean(self._globalConfig.silent) then
				message ..= "\n" .. if Boolean.toJSBoolean(wasInterrupted)
					then chalk.bold(chalk.red("Test run was interrupted."))
					else self:_getTestSummary(contexts, self._globalConfig)
			end
			self:log(message)
		end
	end
end

function SummaryReporter:_printSnapshotSummary(snapshots: SnapshotSummary, globalConfig: Config_GlobalConfig)
	if
		Boolean.toJSBoolean(snapshots.added)
		or Boolean.toJSBoolean(snapshots.filesRemoved)
		or Boolean.toJSBoolean(snapshots.unchecked)
		or Boolean.toJSBoolean(snapshots.unmatched)
		or Boolean.toJSBoolean(snapshots.updated)
	then
		local updateCommand
		local event = Boolean.toJSBoolean(npm_lifecycle_event) and npm_lifecycle_event or ""
		local prefix = Boolean.toJSBoolean(NPM_EVENTS:has(event)) and "" or "run "
		local isYarn = typeof(npm_config_user_agent) == "string"
			and string.match(npm_config_user_agent or "", "yarn") ~= nil
		local client = if Boolean.toJSBoolean(isYarn) then "yarn" else "npm"
		local scriptUsesJest = typeof(npm_lifecycle_script) == "string"
			and Boolean.toJSBoolean(string.match(npm_lifecycle_script or "", "jest"))
		if globalConfig.watch or globalConfig.watchAll then
			updateCommand = "press `u`"
		elseif Boolean.toJSBoolean(event) and scriptUsesJest then
			updateCommand = ("run `%s -u`"):format(client .. " " .. prefix .. event .. (isYarn and "" or " --"))
		else
			updateCommand = "re-run jest with `-u`"
		end
		local snapshotSummary = getSnapshotSummary(snapshots, globalConfig, updateCommand)
		Array.forEach(snapshotSummary, function(summary)
			self:log(summary)
		end)
		self:log("") -- print empty line
	end
end

function SummaryReporter:_printSummary(aggregatedResults: AggregatedResult, globalConfig: Config_GlobalConfig)
	-- If there were any failing tests and there was a large number of tests
	-- executed, re-print the failing results at the end of execution output.
	local failedTests = aggregatedResults.numFailedTests or 0
	local runtimeErrors = aggregatedResults.numRuntimeErrorTestSuites or 0
	if failedTests + runtimeErrors > 0 and aggregatedResults.numTotalTestSuites > TEST_SUMMARY_THRESHOLD then
		self:log(chalk.bold("Summary of all failing tests"))
		Array.forEach(aggregatedResults.testResults, function(testResult)
			local failureMessage = testResult.failureMessage
			if Boolean.toJSBoolean(failureMessage) then
				self:_write(getResultHeader(testResult, globalConfig) .. "\n" .. failureMessage .. "\n")
			end
		end)
		self:log("") -- print empty line
	end
end

function SummaryReporter:_getTestSummary(contexts: Set<Context>, globalConfig: Config_GlobalConfig)
	local function getMatchingTestsInfo()
		local prefix = Boolean.toJSBoolean(globalConfig.findRelatedTests) and " related to files matching "
			or " matching "
		-- ROBLOX deviation START: testPathPatternToRegExp not implemented (and not needed I don't think)
		return chalk.dim(prefix)
		-- ROBLOX deviation END
	end
	local testInfo = ""
	if globalConfig.runTestsByPath then
		testInfo = chalk.dim(" within paths")
	elseif Boolean.toJSBoolean(globalConfig.onlyChanged) then
		testInfo = chalk.dim(" related to changed files")
	elseif Boolean.toJSBoolean(globalConfig.testPathPattern) then
		testInfo = getMatchingTestsInfo()
	end
	local nameInfo = ""
	if globalConfig.runTestsByPath then
		nameInfo = " "
			.. Array.join(
				Array.map(globalConfig.nonFlagArgs, function(p)
					return ('"%s"'):format(p)
				end),
				", "
			)
	elseif globalConfig.testNamePattern ~= nil then
		nameInfo = chalk.dim(" with tests matching ") .. ('"%s"'):format(globalConfig.testNamePattern)
	end
	local contextInfo = if contexts.size > 1
		then chalk.dim(" in ") .. tostring(contexts.size) .. chalk.dim(" projects")
		else ""
	return chalk.dim("Ran all test suites") .. testInfo .. nameInfo .. contextInfo .. chalk.dim(".")
end

exports.default = SummaryReporter

return exports