A module for generating test case templates.

 This module incorporates code from the English Wikipedia's "Testcase table"
 module,[1] written by Frietjes [2] with contributions by Mr. Stradivarius [3]
 and Jackmcbarn,[4] and the English Wikipedia's "Testcase rows" module,[5]
 written by Mr. Stradivarius.

 The "Testcase table" and "Testcase rows" modules are released under the
 CC BY-SA 3.0 License [6] and the GFDL.[7]

 License: CC BY-SA 3.0 and the GFDL
 Author: Mr. Stradivarius

 [1] https://en.wikipedia.org/wiki/Module:Testcase_table
 [2] https://en.wikipedia.org/wiki/User:Frietjes
 [3] https://en.wikipedia.org/wiki/User:Mr._Stradivarius
 [4] https://en.wikipedia.org/wiki/User:Jackmcbarn
 [5] https://en.wikipedia.org/wiki/Module:Testcase_rows
 [6] https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License
 [7] https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_GNU_Free_Documentation_License
]]

-- Load required modules
local yesno = require('Module:Yesno')

-- Set constants
local DATA_MODULE = 'Module:Template test case/data'

-- Shared methods

local function message
	-- This method is added to classes that need to deal with messages from the
	-- config module.
	local msg = self.cfg.msg[key]
	if select then
		return mw.message.newRawMessage:plain
	else
		return msg
	end
end

-- Template class

local Template = {}

Template.memoizedMethods = {
	-- Names of methods to be memoized in each object. This table should only
	-- hold methods with no parameters.
	getFullPage = true,
	getName = true,
	makeHeader = true,
	getOutput = true

function Template.new
	local obj = {}

	-- Set input
	for k, v in pairs(options or {}) do
		if not Template[k] then
			obj[k] = v
		end
	end
	obj._invocation = invocationObj

	-- Validate input
	if not obj.template and not obj.title then
		error
	end

	-- Memoize expensive method calls
	local memoFuncs = {}
	return setmetatable(obj, {
		__index = function 
			if Template.memoizedMethods[key] then
				local func = memoFuncs[key]
				if not func then
					local val = Template[key](t)
					func = function return val end
					memoFuncs[key] = func
				end
				return func
			else
				return Template[key]
			end
		end
end

function Template:getFullPage
	if not self.template then
		return self.title.prefixedText
	elseif self.template:sub == '#invoke' then
		return 'Module' .. self.template:sub(8):gsub
	else
		local strippedTemplate, hasColon = self.template:gsub
		hasColon = hasColon > 0
		local ns = strippedTemplate:match('^(.-):')
		ns = ns and mw.site.namespaces[ns]
		if ns then
			return strippedTemplate
		elseif hasColon then
			return strippedTemplate -- Main namespace
		else
			return mw.site.namespaces[10].name .. ':' .. strippedTemplate
		end
	end
end

function Template:getName
	if self.template then
		return self.template
	else
		return require('Module:Template invocation').name(self.title)
	end
end

function Template:makeLink(display)
	if display then
		return string.format
	else
		return string.format
	end
end

function Template:makeBraceLink(display)
	display = display or self:getName
	local link = self:makeLink(display)
	return mw.text.nowiki('')
end

function Template:makeHeader
	return self.heading or self:makeBraceLink
end

function Template:getInvocation(format)
	local invocation = self._invocation:getInvocation{
		template = self:getName,
		requireMagicWord = self.requireMagicWord,
	if format == 'code' then
		invocation = ''
	elseif format == 'kbd' then
		invocation = ''
	elseif format == 'plain' then
		invocation = mw.text.nowiki(invocation)
	else
		-- Default is pre tags
		invocation = mw.text.encode
		invocation = ''
		invocation = mw.getCurrentFrame:preprocess(invocation)
	end
	return invocation
end

function Template:getOutput
	local protect = require('Module:Protect')
	-- calling self._invocation:getOutput{...}
	return protect(self._invocation.getOutput)(self._invocation, {
		template = self:getName,
		requireMagicWord = self.requireMagicWord,
end

-- TestCase class

local TestCase = {}
TestCase.__index = TestCase
TestCase.message = message -- add the message method

TestCase.renderMethods = {
	-- Keys in this table are values of the "format" option, values are the
	-- method for rendering that format.
	columns = 'renderColumns',
	rows = 'renderRows',
	tablerows = 'renderRows',
	inline = 'renderInline',
	cells = 'renderCells',
	default = 'renderDefault'

function TestCase.new
	local obj = setmetatable
	obj.cfg = cfg

	-- Separate general options from template options. Template options are
	-- numbered, whereas general options are not.
	local generalOptions, templateOptions = {}, {}
	for k, v in pairs(options) do
		local prefix, num
		if type(k) == 'string' then
			prefix, num = k:match('^(.-)([1-9][0-9]*)$')
		end
		if prefix then
			num = tonumber(num)
			templateOptions[num] = templateOptions[num] or {}
			templateOptions[num][prefix] = v
		else
			generalOptions[k] = v
		end
	end

	-- Set general options
	generalOptions.showcode = yesno(generalOptions.showcode)
	generalOptions.showheader = yesno(generalOptions.showheader) ~= false
	generalOptions.showcaption = yesno(generalOptions.showcaption) ~= false
	generalOptions.collapsible = yesno(generalOptions.collapsible)
	generalOptions.notcollapsed = yesno(generalOptions.notcollapsed)
	generalOptions.wantdiff = yesno(generalOptions.wantdiff) 
	obj.options = generalOptions

	-- Preprocess template args
	for num, t in pairs(templateOptions) do
		if t.showtemplate ~= nil then
			t.showtemplate = yesno(t.showtemplate)
		end
	end

	-- Set up first two template options tables, so that if only the
	-- "template3" is specified it isn't made the first template when the
	-- the table options array is compressed.
	templateOptions[1] = templateOptions[1] or {}
	templateOptions[2] = templateOptions[2] or {}

	-- Allow the "template" option to override the "template1" option for
	-- backwards compatibility with Module:Testcase table.
	if generalOptions.template then
		templateOptions[1].template = generalOptions.template
	end

	-- Add default template options
	if templateOptions[1].template and not templateOptions[2].template then
		templateOptions[2].template = templateOptions[1].template ..
			'/' .. obj.cfg.sandboxSubpage
	end
	if not templateOptions[1].template then
		templateOptions[1].title = mw.title.getCurrentTitle.basePageTitle
	end
	if not templateOptions[2].template then
		templateOptions[2].title = templateOptions[1].title:subPageTitle(
			obj.cfg.sandboxSubpage
		)
	end

	-- Remove template options for any templates where the showtemplate
	-- argument is false. This prevents any output for that template.
	for num, t in pairs(templateOptions) do
		if t.showtemplate == false then
			templateOptions[num] = nil
		end
	end

	-- Check for missing template names.
	for num, t in pairs(templateOptions) do
		if not t.template and not t.title then
			error(obj:message, 2)
		end
	end

	-- Compress templateOptions table so we can iterate over it with ipairs.
	templateOptions = (function (t)
		local nums = {}
		for num in pairs(t) do
			nums[#nums + 1] = num
		end
		table.sort(nums)
		local ret = {}
		for i, num in ipairs(nums) do
			ret[i] = t[num]
		end
		return ret
	end)(templateOptions)

	-- Don't require the __TEMPLATENAME__ magic word for nowiki invocations if
	-- there is only one template being output.
	if #templateOptions <= 1 then
		templateOptions[1].requireMagicWord = false
	end

	mw.logObject(templateOptions)

	-- Make the template objects
	obj.templates = {}
	for i, options in ipairs(templateOptions) do
		table.insert(obj.templates, Template.new)
	end

	-- Add tracking categories. At the moment we are only tracking templates
	-- that use any "heading" parameters or an "output" parameter.
	obj.categories = {}
	--for k, v in pairs(options) do
	--	if type(k) == 'string' and k:find('heading') then
	--		obj.categories['Test cases using heading parameters'] = true
	--	elseif k == 'output' then
	--		obj.categories['Test cases using output parameter'] = true
	--	end
	--end

	return obj
end

function TestCase:getTemplateOutput(templateObj)
	local output = templateObj:getOutput
	if self.options.resetRefs then
		mw.getCurrentFrame:extensionTag('references')
	end
	return output
end

function TestCase:templateOutputIsEqual
	-- Returns a boolean showing whether all of the template outputs are equal.
	-- The random parts of strip markers (see Help:Strip markers) are
	-- removed before comparison. This means a strip marker can contain anything
	-- and still be treated as equal, but it solves the problem of otherwise
	-- identical wikitext not returning as exactly equal.
	local function normaliseOutput(obj)
		local out = obj:getOutput
		-- Remove the random parts from strip markers.
		out = out:gsub('(\127[^\127]*UNIQ%-%-%l+%-)%x+(%-%-?QINU[^\127]*\127)', '%1%2')
		return out
	end
	local firstOutput = normaliseOutput(self.templates[1])
	for i = 2, #self.templates do
		local output = normaliseOutput(self.templates[i])
		if output ~= firstOutput then
			return false
		end
	end
	return true
end

function TestCase:makeCollapsible(s)
	local title = self.options.title or self.templates[1]:makeHeader
	if self.options.titlecode then
		title = self.templates[1]:getInvocation('kbd')
	end
	local isEqual = self:templateOutputIsEqual
	local root = mw.html.create('div')
	root
	if self.options.wantdiff then
		root
	else
		if self.options.notcollapsed ~= true or false then
			root
		end
		root
	end
	root
	return tostring(root)
end

function TestCase:renderColumns
	local root = mw.html.create
	if self.options.showcode then
		root
	end

	local tableroot = root:tag('table')

	if self.options.showheader then
		-- Caption
		if self.options.showcaption then
			tableroot
		end

		-- Headers
		local headerRow = tableroot:tag('tr')
		if self.options.rowheader then
			-- rowheader is correct here. We need to add another th cell if
			-- rowheader is set further down, even if heading0 is missing.
			headerRow:tag('th'):wikitext(self.options.heading0)
		end
		local width
		if #self.templates > 0 then
			width = tostring(math.floor(100 / #self.templates)) .. '%'
		else
			width = '100%'
		end
		for i, obj in ipairs(self.templates) do
			headerRow
		end
	end

	-- Row header
	local dataRow = tableroot:tag('tr'):css
	if self.options.rowheader then
		dataRow:tag('th')
	end
	
	-- Template output
	for i, obj in ipairs(self.templates) do
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
		else
			dataRow:tag('td')
		end
	end
	
	return tostring(root)
end

function TestCase:renderRows
	local root = mw.html.create
	if self.options.showcode then
		root
	end

	local tableroot = root:tag('table')
	tableroot

	if self.options.caption then
		tableroot
	end

	for _, obj in ipairs(self.templates) do
		local dataRow = tableroot:tag('tr')
		
		-- Header
		if self.options.showheader then
			if self.options.format == 'tablerows' then
				dataRow:tag('th')
				dataRow:tag('td')
			else
				dataRow:tag('td')
				dataRow = tableroot:tag('tr')
			end
		end
		
		-- Template output
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
		else
			dataRow:tag('td')
		end
	end

	return tostring(root)
end

function TestCase:renderInline
	local arrow = mw.language.getContentLanguage:getArrow('forwards')
	local ret = {}
	for i, obj in ipairs(self.templates) do
		local line = {}
		line[#line + 1] = self.options.prefix or '* '
		if self.options.showcode then
			line[#line + 1] = obj:getInvocation('code')
			line[#line + 1] = ' '
			line[#line + 1] = arrow
			line[#line + 1] = ' '
		end
		if self.options.output == 'nowiki+' then
			line[#line + 1] = self.options.before or ""
			line[#line + 1] = self:getTemplateOutput(obj)
			line[#line + 1] = self.options.after or ""
			line[#line + 1] = ''
		elseif self.options.output == 'nowiki' then
			line[#line + 1] = mw.text.nowiki(self.options.before or "")
			line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
			line[#line + 1] = mw.text.nowiki(self.options.after or "")
		else
			line[#line + 1] = self.options.before or ""
			line[#line + 1] = self:getTemplateOutput(obj)
			line[#line + 1] = self.options.after or ""
		end
		ret[#ret + 1] = table.concat(line)
	end
	if self.options.addline then
		local line = {}
		line[#line + 1] = self.options.prefix or '* '
		line[#line + 1] = self.options.addline
		ret[#ret + 1] = table.concat(line)
	end
	return table.concat
end

function TestCase:renderCells
	local root = mw.html.create
	local dataRow = root:tag('tr')
	dataRow

	-- Row header
	if self.options.rowheader then
		dataRow:tag('th')
	end
	-- Caption
	if self.options.showcaption then
		dataRow:tag('th')
	end

	-- Show code
	if self.options.showcode then
		dataRow:tag('td')
	end

	-- Template output
	for i, obj in ipairs(self.templates) do
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
		else
			dataRow:tag('td')
		end
	end

	return tostring(root)
end

function TestCase:renderDefault
	local ret = {}
	if self.options.showcode then
		ret[#ret + 1] = self.templates[1]:getInvocation
	end
	for i, obj in ipairs(self.templates) do
		ret[#ret + 1] = ''
		if self.options.showheader then
			ret[#ret + 1] = obj:makeHeader
		end
		if self.options.output == 'nowiki+' then
			ret[#ret + 1] = (self.options.before or "") ..
			self:getTemplateOutput(obj) ..
			(self.options.after or "") ..
			''
		elseif self.options.output == 'nowiki' then
			ret[#ret + 1] = mw.text.nowiki(self.options.before or "") ..
			mw.text.nowiki(self:getTemplateOutput(obj)) ..
			mw.text.nowiki(self.options.after or "")
		else
			ret[#ret + 1] = (self.options.before or "") ..
			self:getTemplateOutput(obj) ..
			(self.options.after or "")
		end
	end
	return table.concat
end

function TestCase:__tostring
	local format = self.options.format
	local method = format and TestCase.renderMethods[format] or 'renderDefault'
	local ret = self[method](self)
	if self.options.collapsible then
		ret = self:makeCollapsible(ret)
	end
	for cat in pairs(self.categories) do
		ret = ret .. string.format
	end
	return ret
end

-- Nowiki invocation class

local NowikiInvocation = {}
NowikiInvocation.__index = NowikiInvocation
NowikiInvocation.message = message -- Add the message method

function NowikiInvocation.new
	local obj = setmetatable
	obj.cfg = cfg
	invocation = mw.text.unstrip(invocation)
	-- Decode HTML entities for , and ". This means that HTML entities in
	-- the original code must be escaped as e.g. &amp;lt;, which is unfortunate,
	-- but it is the best we can do as the distinction between , " and &lt;,
	-- &gt;, &quot; is lost during the original nowiki operation.
	invocation = invocation:gsub
	invocation = invocation:gsub
	invocation = invocation:gsub
	obj.invocation = invocation
	return obj
end

function NowikiInvocation:getInvocation(options)
	local template = options.template:gsub -- Escape "%" with "%%"
	local invocation, count = self.invocation:gsub
	if options.requireMagicWord ~= false and count < 1 then
		error(self:message)
	end
	return invocation
end

function NowikiInvocation:getOutput(options)
	local invocation = self:getInvocation(options)
	return mw.getCurrentFrame:preprocess(invocation)
end

-- Table invocation class

local TableInvocation = {}
TableInvocation.__index = TableInvocation
TableInvocation.message = message -- Add the message method

function TableInvocation.new
	local obj = setmetatable
	obj.cfg = cfg
	obj.invokeArgs = invokeArgs
	obj.code = nowikiCode
	return obj
end

function TableInvocation:getInvocation(options)
	if self.code then
		local nowikiObj = NowikiInvocation.new
		return nowikiObj:getInvocation(options)
	else
		return require('Module:Template invocation').invocation
	end
end

function TableInvocation:getOutput(options)
	if (options.template:sub == '#invoke') then
		local moduleCall = mw.text.split
		local args = mw.clone(self.invokeArgs)
		table.insert
		return mw.getCurrentFrame:callParserFunction
	end
	return mw.getCurrentFrame:expandTemplate{
		title = options.template,
		args = self.invokeArgs
end

-- Bridge functions
--
-- These functions translate template arguments into forms that can be accepted
-- by the different classes, and return the results.

local bridge = {}

function bridge.table
	cfg = cfg or mw.loadData(DATA_MODULE)

	local options, invokeArgs = {}, {}
	for k, v in pairs(args) do
		local optionKey = type(k) == 'string' and k:match('^_(.*)$')
		if optionKey then
			if type(v) == 'string' then
				v = v:match('^%s*(.-)%s*$') -- trim whitespace
			end
			if v ~= '' then
				options[optionKey] = v
			end
		else
			invokeArgs[k] = v
		end
	end

	-- Allow passing a nowiki invocation as an option. While this means users
	-- have to pass in the code twice, whitespace is preserved and &lt; etc.
	-- will work as intended.
	local nowikiCode = options.code
	options.code = nil

	local invocationObj = TableInvocation.new
	local testCaseObj = TestCase.new
	return tostring(testCaseObj)
end

function bridge.nowiki
	cfg = cfg or mw.loadData(DATA_MODULE)

	local code = args.code or args[1]
	local invocationObj = NowikiInvocation.new
	args.code = nil
	args[1] = nil
	-- Assume we want to see the code as we already passed it in.
	args.showcode = args.showcode or true
	local testCaseObj = TestCase.new
	return tostring(testCaseObj)
end

-- Exports

local p = {}

function p.main
	cfg = cfg or mw.loadData(DATA_MODULE)

	-- Load the wrapper config, if any.
	local wrapperConfig
	if frame.getParent then
		local title = frame:getParent:getTitle
		local template = title:gsub
		wrapperConfig = cfg.wrappers[template]
	end

	-- Work out the function we will call, use it to generate the config for
	-- Module:Arguments, and use Module:Arguments to find the arguments passed
	-- by the user.
	local func = wrapperConfig and wrapperConfig.func or 'table'
	local userArgs = require('Module:Arguments').getArgs(frame, {
		parentOnly = wrapperConfig,
		frameOnly = not wrapperConfig,
		trim = func ~= 'table',
		removeBlanks = func ~= 'table'

	-- Get default args and build the args table. User-specified args overwrite
	-- default args.
	local defaultArgs = wrapperConfig and wrapperConfig.args or {}
	local args = {}
	for k, v in pairs(defaultArgs) do
		args[k] = v
	end
	for k, v in pairs(userArgs) do
		args[k] = v
	end

	return bridge[func]
end

function p._exportClasses -- For testing
	return {
		Template = Template,
		TestCase = TestCase,
		NowikiInvocation = NowikiInvocation,
		TableInvocation = TableInvocation
end