local CodeCoverage = script.Parent
local LineScanner = require(CodeCoverage.LineScanner)
local LcovReporter = require(CodeCoverage.LcovReporter)

local ScriptContext = game:GetService("ScriptContext")
local CoreScriptSyncService = game:GetService("CoreScriptSyncService")

local Reporter = {}
Reporter.__index = Reporter

function Reporter.processCoverageStats()
	local stats = ScriptContext:GetCoverageStats()

	local files = {}
	for _, scriptStats in ipairs(stats) do
		local aScript = scriptStats.Script;

		local source = aScript.Source

		local hits = scriptStats.Hits
		local lineHit = 0
		local lineMissed = 0
		local lines = {}

		if scriptStats.HitsPrecise then
			local sources = source:split('\n')

			for n,h in ipairs(hits) do
				local ignored = h < 0

				if h > 0 then
					lineHit = lineHit + 1
				elseif h == 0 then
					lineMissed = lineMissed + 1
				end

				lines[n] = {
					source = sources[n],
					ignored = ignored,
					hits = math.max(h, 0)
				}
			end
		else
			local scanner = LineScanner:new()
			local lineNumber = 1

			for line in source:gmatch('([^\r\n]*)[\r\n]?') do
				local excluded, excludedIfNotHit = scanner:consume(line)

				local ignored = excluded

				if not excluded then
					if hits[lineNumber] and hits[lineNumber] > 0 then
						lineHit = lineHit + 1
					else
						if excludedIfNotHit then
							ignored = true
						else
							lineMissed = lineMissed + 1
						end
					end
				end

				lines[lineNumber] = {
					source = line,
					ignored = ignored,
					hits = hits[lineNumber] or 0
				}

				lineNumber = lineNumber + 1
			end
		end

		table.insert(files, {
			script = aScript,
			path = CoreScriptSyncService:GetScriptFilePath(aScript),
			lines = lines,
			hits = lineHit,
			misses = lineMissed,
		})
	end

	return files
end

local function matchesAny(str, excludes)
	if not str or str:len() == 0 or not excludes then
		return false
	end

	for _,exclude in ipairs(excludes) do
		if string.find(str, exclude) ~= nil then
			return true
		end
	end
	return false
end

function Reporter.generateReport(path, excludes)
	local report = LcovReporter.generate(Reporter.processCoverageStats(), function(file)
		local isExcluded = file.script.Name:match(".spec$")
			or file.script:FindFirstAncestor("TestEZ")
			or file.script:IsDescendantOf(CodeCoverage)
			or matchesAny(file.path, excludes)
		local isIncluded = file.path and file.path:len() > 0

		return isIncluded and not isExcluded
	end)
	if report:len() == 0 then
		warn("Generating code coverage report failed. Produced report has zero size.")
		return
	end

	local success, message = pcall(function() -- New API
		local fs = game:GetService("FileSystemService")
		fs:WriteFile(path, report)
	end)

	if not success then
		warn("Failed to save code coverage report at path: " .. path .. "\nError: " .. message)
	end
end

return Reporter
