-- ROBLOX deviation: Added _process arg to mock global node process for testing
function DefaultReporter.new(globalConfig: Config_GlobalConfig, _process: NodeProcessMock?): DefaultReporter
	local self = setmetatable((BaseReporter.new(_process) :: any) :: DefaultReporterPrivate, DefaultReporter)
	-- ROBLOX deviation: we need to store _isInteractive per reporter to not override isInteractive globally
	self._isInteractive = jestUtilModule.isInteractive

	-- ROBLOX deviation START: Added for tests otherwise isInteractive = false
	if _process ~= nil and _process.env.IS_INTERACTIVE ~= nil then
		self._isInteractive = _process.env.IS_INTERACTIVE
	end
	-- ROBLOX deviation END

	-- ROBLOX deviation START: Need a copy of stdout/stderr write since
	-- they get overwritten in _wrapStdio.
	local function defaultWrite()
		local writable = Writeable.new()

		return function(_self: Writeable, message: string)
			writable:write(message)
		end
	end
	--[[
		ROBLOX FIXME Luau START: added explicit type declaration.
		Otherwise Luau can't seem to unify 2 function type declaration
		and infers this as an union of function types which is not considered callable by CLI's analyze
	]]
	local _stdoutWrite: (Writeable, string) -> () = if _process and _process.stdout
		then _process.stdout.write
		else defaultWrite()
	local _stderrWrite: (Writeable, string) -> () = if _process and _process.stderr
		then _process.stderr.write
		else defaultWrite()
	-- ROBLOX FIXME Luau END
	-- ROBLOX deviation END

	self._globalConfig = globalConfig
	self._clear = ""
	self._out = function(_self: Writeable, message: string)
		_stdoutWrite(self._process.stdout, message)
	end
	self._err = function(_self: Writeable, message: string)
		_stderrWrite(self._process.stderr, message)
	end
	self._status = Status.new()
	self._bufferedOutput = Set.new()
	self:__wrapStdio(self._process.stdout)
	self:__wrapStdio(self._process.stderr)
	self._status:onChange(function()
		self:__clearStatus()
		self:__printStatus()
	end)

	return (self :: any) :: DefaultReporter
end

function DefaultReporter:__wrapStdio(stream: Writeable): ()
	local write = stream.write

	local buffer: Array<string> = {}
	local timeout: Timeout | nil = nil

	local function flushBufferedOutput()
		local string_ = Array.join(buffer, "")
		buffer = {}

		-- This is to avoid conflicts between random output and status text
		self:__clearStatus()
		if Boolean.toJSBoolean(string_) then
			write(stream, string_)
		end
		self:__printStatus()
		self._bufferedOutput:delete(flushBufferedOutput)
	end

	self._bufferedOutput:add(flushBufferedOutput)

	local function debouncedFlush()
		-- If the process blows up no errors would be printed.
		-- There should be a smart way to buffer stderr, but for now
		-- we just won't buffer it.
		if stream == self._process.stderr then
			flushBufferedOutput()
		elseif not Boolean.toJSBoolean(timeout) then
			timeout = setTimeout(function()
				flushBufferedOutput()
				timeout = nil
			end, 100)
		end
	end

	stream.write = function(_self: Writeable, chunk: string)
		table.insert(buffer, chunk)
		debouncedFlush()
		return true
	end
end

function DefaultReporter:forceFlushBufferedOutput(): ()
	Set.forEach(self._bufferedOutput, function(flushBufferedOutput)
		flushBufferedOutput()
	end)
end

function DefaultReporter:__clearStatus(): ()
	-- ROBLOX deviation: we need to store _isInteractive per reporter to not override isInteractive globally
	if self._isInteractive then
		if self._globalConfig.useStderr then
			self:_err(self._clear)
		else
			self:_out(self._clear)
		end
	end
end

function DefaultReporter:__printStatus(): ()
	local ref = self._status:get()
	local content, clear = ref.content, ref.clear
	self._clear = clear
	-- ROBLOX deviation: we need to store _isInteractive per reporter to not override isInteractive globally
	if self._isInteractive then
		if self._globalConfig.useStderr then
			self:_err(content)
		else
			self:_out(content)
		end
	end
end

function DefaultReporter:onRunStart(aggregatedResults: AggregatedResult, options: ReporterOnStartOptions): ()
	self._status:runStarted(aggregatedResults, options)
end

function DefaultReporter:onTestStart(test: Test): ()
	self._status:testStarted(test.path, test.context.config)
end

function DefaultReporter:onTestCaseResult(test: Test, testCaseResult: TestCaseResult): ()
	self._status:addTestCaseResult(test, testCaseResult)
end

function DefaultReporter:onRunComplete(): ()
	self:forceFlushBufferedOutput()
	self._status:runFinished()
	self._process.stdout.write = self._out
	self._process.stderr.write = self._err
	clearLine(self._process.stderr)
end

function DefaultReporter:onTestResult(test: Test, testResult: TestResult, aggregatedResults: AggregatedResult): ()
	self:testFinished(test.context.config, testResult, aggregatedResults)
	if not testResult.skipped then
		self:printTestFileHeader(testResult.testFilePath, test.context.config, testResult)
		self:printTestFileFailureMessage(testResult.testFilePath, test.context.config, testResult)
	end
	self:forceFlushBufferedOutput()
end

function DefaultReporter:testFinished(
	config: Config_ProjectConfig,
	testResult: TestResult,
	aggregatedResults: AggregatedResult
): ()
	self._status:testFinished(config, testResult, aggregatedResults)
end

function DefaultReporter:printTestFileHeader(
	_testPath: Config_Path,
	config: Config_ProjectConfig,
	result: TestResult
): ()
	self:log(getResultHeader(result, self._globalConfig, config))
	if result.console ~= nil then
		self:log("  " .. TITLE_BULLET .. "Console\n\n" .. getConsoleOutput(result.console, config, self._globalConfig))
	end
end

function DefaultReporter:printTestFileFailureMessage(
	_testPath: Config_Path,
	_config: Config_ProjectConfig,
	result: TestResult
): ()
	if Boolean.toJSBoolean(result.failureMessage) then
		self:log(result.failureMessage)
	end
	local didUpdate = self._globalConfig.updateSnapshot == "all"
	local snapshotStatuses = getSnapshotStatus(result.snapshot, didUpdate)
	Array.forEach(snapshotStatuses, self.log, self)
end

exports.default = DefaultReporter

return exports