-- The expected text must be in a single line, but can include
-- "\n" (two characters) to indicate that a newline is expected.
-- Tests are run (or created) by setting p.tests (string or table), or
-- by setting page=PAGE_TITLE (and optionally section=SECTION_TITLE),
-- then executing run_tests (or make_tests).

local Collection = {}
Collection.__index = Collection
do
	function Collection:add(item)
		if item ~= nil then
			self.n = self.n + 1
			self[self.n] = item
		end
	end
	function Collection:join(sep)
		return table.concat
	end
	function Collection.new
		return setmetatable
	end
end

local function empty(text)
	-- Return true if text is nil or empty (assuming a string).
	return text == nil or text == ''
end

local function strip(text)
	-- Return text with no leading/trailing whitespace.
	return text:match("^%s*(.-)%s*$")
end

local function normalize(text)
	-- Return text with any strip markers normalized by replacing the
	-- unique number with a fixed value so comparisons work.
	return text:gsub('(\127[^\127]*UNIQ[^\127]*%-)(%x\+)(-QINU[^\127]*\127)', '%100000000%3')
end

local function status_box
	local label, bgcolor, align, isfail
	if iscomment then
		actual = ''
		align = 'center'
		bgcolor = 'silver'
		label = 'Cmnt'
	elseif expected == '' then
		stats.ignored = stats.ignored + 1
		return '', actual
	elseif normalize(expected) == normalize(actual) then
		stats.pass = stats.pass + 1
		actual = ''
		align = 'center'
		bgcolor = 'green'
		label = 'Pass'
	else
		stats.fail = stats.fail + 1
		align = 'center'
		bgcolor = 'red'
		label = 'Fail'
		isfail = true
	end
	local sbox = 'style="text-align:' .. align .. ';color:white;background:' .. bgcolor .. ';" | ' .. label
	return sbox, actual, isfail
end

local function status_text(stats)
	local bgcolor, ignored_text, msg, ttext
	if stats.template then
		ttext = "**Using Template:" .. stats.template .. ":** "
	else
		ttext = ''
	end
	if stats.fail == 0 then
		if stats.pass == 0 then
			bgcolor = 'salmon'
			msg = 'No tests performed'
		else
			bgcolor = 'green'
			msg = string.format
		end
	else
		bgcolor = 'darkred'
		msg = string.format
	end
	if stats.ignored == 0 then
		ignored_text = ''
	else
		bgcolor = 'salmon'
		ignored_text = string.format(', %d test%s 무시됨 (예상한 텍스트가 공백이므로)', stats.ignored, stats.ignored == 1 and '' or 's')
	end
	return ttext .. ''
end

local function run_template
	-- Template ""
	-- gives xargs { " abc ", "def", name = "ghi jkl" }.
	if template:sub == '' then
		template = template:sub .. '|' -- append sentinel to get last field
	else
		return '(invalid template)'
	end
	local xargs = {}
	local index = 1
	local templatename
	local function put_arg
		-- Kludge: Module:Val uses Module:Arguments which trims arguments and
		-- omits blank arguments. Simulate that here.
		-- LATER Need a parameter to control this.
		if templatename:sub == 'val' then
			v = strip(v)
			if v == '' then
				return
			end
		end
		xargs[k] = v
	end
	template = template:gsub('(%[%^%[%-)|(.-%]%])', '%1\0%2') -- replace pipe in piped link with a zero byte
	for field in template:gmatch('(.-)|') do
		field = field:gsub -- restore pipe in piped link
		if templatename == nil then
			templatename = args.template or strip(field)
			if templatename == '' then
				return '(invalid template)'
			end
		else
			local k, eq, v = field:match("^(.-)(=)(.*)$")
			if eq then
				k, v = strip(k), strip(v) -- k and/or v can be empty
				local i = tonumber(k)
				if i and i > 0 and string.match then
					put_arg
				else
					put_arg
				end
			else
				while xargs[index] ~= nil do
					-- Skip any explicit numbered parameters like "|5=five".
					index = index + 1
				end
				put_arg
			end
		end
	end
	if args.test and not xargs.test then
		-- For convert, allow test=preview or test=nopreview to be injected into
		-- the convert under test, if it does not already use that parameter.
		-- That allows, for example, a preview of make_tests to show nopreview results.
		xargs.test = args.test
	end
	local function expand(t)
		return frame:expandTemplate(t)
	end
	local ok, result = pcall
	if not ok then
		result = 'Error: ' .. result
	end
	if collapse_multiline then
		result = result:gsub
	end
	return result
end

local function _make_tests
	local maxlen = 38
	for _, item in ipairs(all_tests) do
		local template = item[1]
		if template then
			local templen = mw.ustring.len(template)
			item.templen = templen
			if maxlen < templen and templen <= 70 then
				maxlen = templen
			end
		end
	end
	local result = Collection.new
	for _, item in ipairs(all_tests) do
		local template = item[1]
		if template then
			local actual = run_template
			local pad = string.rep .. ' '
			result:add(template .. pad .. actual)
		else
			local text = item.text
			if text then
				result:add(text)
			end
		end
	end
	-- Pre tags returned by a module are html tags, not like wikitext .
	return ''
end

local function _run_tests
	local function safe_cell
		-- For testing , want wikitext like 'kg' to be unchanged
		-- so the link works and so the displayed text is short (just "kg" in example).
		text = text:gsub('(%[%^%[%-)|(.-%]%])', '%1\0%2') -- replace pipe in piped link with a zero byte
		text = text:gsub:gsub -- escape '{' and '|'
		text = text:gsub -- restore pipe in piped link
		if multiline then
			text = text:gsub
		end
		return text
	end
	local function nowiki_cell
		text = mw.text.nowiki(text)
		if multiline then
			text = text:gsub
		end
		return text
	end
	local stats = { pass = 0, fail = 0, ignored = 0, template = args.template }
	local result = Collection.new
	result:add('{| class="wikitable sortable"')
	result:add('! 틀 !! 예상 !! 실제 (다른 경우) !! 상태')
	for _, item in ipairs(all_tests) do
		local template, expected = item[1], item[2] or ''
		if template then
			local actual = run_template
			local sbox, actual, isfail = status_box
			result:add('|-')
			result:add('| ' .. safe_cell(template))
			result:add('| ' .. safe_cell)
			result:add('| ' .. safe_cell)
			result:add('| ' .. sbox)
			if isfail then
				result:add('|-')
				result:add('| align="center"| ')
				result:add('| ' .. nowiki_cell(normalize(expected), true))
				result:add('| ' .. nowiki_cell(normalize(actual), true))
				result:add('|')
			end
		else
			local text = item.text
			if text and text:sub == '---' then
				result:add('|-')
				result:add('| colspan="3" style="color:white;background:silver;" | ' .. safe_cell(strip(text:sub(4)), true))
				result:add('| ' .. status_box)
			end
		end
	end
	result:add('|}')
	return status_text(stats) .. '\n\n' .. result:join('\n')
end

local function get_page_content
	local t = mw.title.new(page_title)
	if t then
		local content = t:getContent
		if content then
			if content:sub(-1) ~= '\n' then
				content = content .. '\n'
			end
			return content
		end
	end
	if not ignore_error then
		error
	end
end

local function _compare
	local function diff_link
		return ''
	end
	local function link(title)
		return '' .. title .. ''
	end
	local function message
		local color = isgood and 'green' or 'darkred'
		return ''
	end
	local result = Collection.new
	for _, item in ipairs(page_pairs) do
		local label
		local title1 = item[1]
		local title2 = item[2]
		if title1 == title2 then
			label = message
		else
			local content1 = get_page_content
			local content2 = get_page_content
			if not content1 or not content2 then
				label = message
			elseif content1 == content2 then
				label = message
			else
				label = message .. ' (' .. diff_link .. ')'
			end
		end
		result:add('*' .. link(title1) .. ' • ' .. link(title2) .. ' • ' .. label)
	end
	return result:join('\n')
end

local function sections(text)
	return {
		first = 1, -- just after the newline at the end of the last heading
		this_section = 1,
		next_heading = function(self)
			local first = self.first
			while first <= #text do
				local last, heading
				first, last, heading = text:find('==+[\t ]*([^\n]-)[\t ]*==+[\t\r ]*\n', first)
				if first then
					if first == 1 or text:sub == '\n' then
						self.this_section = first
						self.first = last + 1
						return heading
					end
					first = last + 1
				else
					break
				end
			end
			self.first = #text + 1
			return nil
		end,
		current_section = function(self)
			local first = self.this_section
			local last = text:find
			if not last then
				last = -1
			end
			return text:sub
		end,
end

local function get_tests
	local args = frame.args
	local page_title, section_title = args.page, args.section
	local show_all = (args.show == 'all')
	if not empty(page_title) then
		if not empty(tests) then
			error
		end
		if page_title:sub == '' and page_title:sub(-2) == '' then
			page_title = strip(page_title:sub)
		end
		tests = get_page_content(page_title)
		if not empty(section_title) then
			local s = sections(tests)
			while true do
				local heading = s:next_heading
				if heading then
					if heading == section_title then
						tests = s:current_section
						break
					end
				else
					error
				end
			end
		end
	end
	if type(tests) ~= 'string' then
		if type(tests) == 'table' then
			return tests
		end
		error
	end
	if tests:sub(-1) ~= '\n' then
		tests = tests .. '\n'
	end
	local template_count = 0
	local all_tests = Collection.new
	for line in (tests):gmatch('([^\n]-)[\t\r ]*\n') do
		local template, expected = line:match('^%s*(.-)%s*$')
		if template then
			template_count = template_count + 1
			all_tests:add
		elseif show_all then
			all_tests:add({ text = line })
		end
	end
	if template_count == 0 then
		error
	end
	return all_tests
end

local function main
	local ok, result = pcall
	if ok then
		ok, result = pcall
		if ok then
			return result
		end
	end
	return '\n\n' .. result
end

local modules = {
	-- For convenience, a key defined here can be used to refer to the
	-- corresponding list of modules.
	countries = { -- Commons
		'Countries',
		'Countries/Africa',
		'Countries/Americas',
		'Countries/Arab world',
		'Countries/Asia',
		'Countries/Caribbean',
		'Countries/Central America',
		'Countries/Europe',
		'Countries/North America',
		'Countries/North America (subcontinent)',
		'Countries/Oceania',
		'Countries/South America',
		'Countries/United Kingdom',
	convert = {
		'Convert',
		'Convert/data',
		'Convert/text',
		'Convert/extra',
		'Convert/wikidata',
		'Convert/wikidata/data',
	cs1 = {
		'Citation/CS1',
		'Citation/CS1/Configuration',
	cs1all = {
		'Citation/CS1',
		'Citation/CS1/Configuration',
		'Citation/CS1/Whitelist',
		'Citation/CS1/Date validation',
	team = {
		'Team appearances list',
		'Team appearances list/data',
		'Team appearances list/show',
	val = {
		'Val',
		'Val/units',

local p = {}

function p.compare(frame)
	local page_pairs = p.pairs
	if not page_pairs then
		local args = frame.args
		if not args[2] then
			local builtins = modules[args[1] or 'convert']
			if builtins then
				args = builtins
			end
		end
		page_pairs = {}
		for i, title in ipairs(args) do
			if not title:find then
				title = '모듈:' .. title
			end
			page_pairs[i] = { title, title .. '/연습장' }
		end
	end
	local ok, result = pcall
	if ok then
		return result
	end
	return '\n\n' .. result
end

p.check_sandbox = p.compare

function p.make_tests(frame)
	return main
end

function p.run_tests(frame)
	return main
end