module(..., package.seeall)

require "coxpcall"

local _tb_hide = setmetatable( {}, {__mode="k"} )

function traceback_hide(func)
	_tb_hide[func] = true
end

local asserts = require "luatestlauncher.asserts"

local __failure__ = {}    -- Type tag for failed assertions

asserts.SetStuff{__failure__ = __failure__}	-- que buen nombre...


local typenames = { "nil", "boolean", "number", "string", "table", "function", "thread", "userdata" }

local string_format   = string.format

-- Type check functions

local orig_assert     = assert


for _, typename in ipairs(typenames) do
  _M["is_"..typename] = function(x)
    return type(x) == typename
  end
end

---
-- Pretty prints an error object or message
local function format_error(err)
	local msg = ""
	if type(err) == "table" then
		if err.type == __failure__ then
			msg = msg .. ("Failure:\n%s: %s\n"):format(err.where, err.msg)
			if err.usermsg then
				msg = msg .. ("%s: %s\n"):format(err.where, err.usermsg)
			end
			msg = msg .. ("\t%s\n"):format(table.concat(err.tb, "\n\t"))
			--xml = xml..(tests[i].error.msg or "Test did not run")..'\n'
		else
			msg = msg .. ("Error!:\n%s\n\t%s\n"):format(err.msg or "(no error message)", table.concat(err.tb, "\n\t"))
		end
	else
		msg = msg..(err or "Test did not run")..'\n'
	end
	return msg
end


---
-- Helper method for capturing stack traces.
local function get_stack_trace(thread, errobj)
	if is_table(errobj) then
		errobj = errobj
	else
		errobj = { msg = errobj and tostring(errobj) or "(no error message)" }
	end
	
	errobj.tb = {}
	local i = 1
	while true do
		local info = thread and debug.getinfo(thread, i, "Snlf") or debug.getinfo(i, "Snlf")
		if not is_table(info) then
			break
		end
		if not _tb_hide[info.func] then
			local line = {}       -- Ripped from ldblib.c...
			line[#line+1] = string_format("%s:", info.short_src)
			if info.currentline > 0 then
				line[#line+1] = string_format("%d:", info.currentline)
			end
			if info.namewhat ~= "" then
				line[#line+1] = string_format(" in function '%s'", info.name)
			else
				if info.what == "main" then
					line[#line+1] = " in main chunk"
				elseif info.what == "C" or info.what == "tail" then
					line[#line+1] = " ?"
				else
					line[#line+1] = string_format(" in function <%s:%d>", info.short_src, info.linedefined)
				end
			end
			errobj.tb[#errobj.tb+1] = table.concat(line)
		end
		i = i + 1
	end
	  
	return errobj
end


---
-- Captures a stack trace for a given thread
local function my_traceback(thread, errobj)
	if type(thread) ~= "thread" then
		errobj = thread
		thread = nil
	end
	
	if is_table(errobj) then
		if errobj.type == __failure__ then
			if not errobj.where then
				local info = thread and debug.getinfo(thread, 3, "Sl") or debug.getinfo(3, "Sl")	-- FIXME: Hardcoded integers are bad...
				errobj.where = string_format( "%s:%d", info.short_src, info.currentline)
				errobj = get_stack_trace(thread, errobj)
			end
		elseif errobj.tb then
			return errobj
		else
			errobj = get_stack_trace(thread, errobj)
		end
	else
		errobj = get_stack_trace(thread, errobj)
	end
	return errobj
end


function TestLauncher (testSuiteName)
	
	local tests = {}
	local testsByName = {}
	local launcher = {}
	local destructors = {}
	local reverseDestructors = {}
	
    --Add a test to the suite
    launcher.AddTest = function (description, test, package)
		package = package or testSuiteName
		assert (type(package) == "string", "Test package must be a string")
        assert (type(description) == "string", "Test description must be a string")
        
		local testName
		local testFunction
		
		if type (test) == "string" then
			testName = test
		elseif type (test) == "table" then
			testName = test.name
			testFunction = test.test
		else
			error ("Parameter test must be a string or a table")
		end
        table.insert(tests, {description = description, test = testName, testFunction = testFunction, package = package})
        testsByName[testName] = description
    end
	
	launcher.AddDestructor = function (f)
		assert (type (f) == "function", "Destructor must be a function")
		table.insert(destructors, f)
	end
    
	launcher.AddReverseDestructor = function (f)
		assert (type (f) == "function", "Destructor must be a function")
		table.insert(reverseDestructors, f)
	end
    
    launcher.Run = function (args)
        
	    local tests2run  = {}
		--remove the tests that are not in the args
		--or leave it as is if the args are malformed
        if type(args) == "table" and #args > 0 then
            
			local function matchSomeFilter(test)
				for _, filter in ipairs(args) do
					if test == test:match(filter) then
						return true
					end
				end
				return false
			end
			
			for _, test in pairs(tests) do
				if matchSomeFilter(test.test) then
					table.insert(tests2run, test)
				end
			end

		else
		   tests2run = tests
        end
        
		--run the tests
        for i = 1, #tests2run do
            
            local test
			if tests2run[i].testFunction then
				test = tests2run[i].testFunction
			else
				test = _G[tests2run[i].test]
			end
			
            assert (type (test) == "function", "Invalid test '"..tests2run[i].test.."'")
            
			--clean the destructors table for this test
			destructors = {}
			reverseDestructors = {}
            
			--calls the BeforeTest function if it was specified
			if type (launcher.BeforeTest) == "function" then
                launcher.BeforeTest (tests2run[i].test)
            end
            
            local t1 = os.time()
            
            --can not call pcall because this run inside a couroutine
            --so we call coxpcall wich is the couroutine friendly pcall version
			local old_trace = debug.traceback
			debug.traceback = my_traceback
			local call_test = function() test(launcher) end
			traceback_hide( call_test )
			local result, err = coxpcall(call_test, my_traceback)
			
            local t2 = os.time()
            
            if result then
                tests2run[i].result = "OK"
            else
                tests2run[i].result = "FAIL"
                tests2run[i].error = err
            end
           
			local function CallDestructor(d)
				local result, err = coxpcall(function () d(tests2run[i].result, tests2run[i].error) end, my_traceback)
				if not result then
					print ("Error running destructor: ", format_error(err))
				end
			end

			--calls the reverse destructors in the reverse order as they where added (LIFO)
			for i = #reverseDestructors, 1, -1 do
				CallDestructor(reverseDestructors[i])
			end
			--calls the destructors in the same order as they where added (FIFO)
			for _, f in ipairs (destructors) do
				CallDestructor(f)
			end
			
			tests2run[i].time = os.difftime(t2, t1)
			
			--calls the AfterTest function if it was specified
            if type (launcher.AfterTest) == "function" then
                launcher.AfterTest (tests2run[i].test, tests2run[i].result, err and format_error(err))
            end
            
            debug.traceback = old_trace
        end
        
        return tests2run
    end
	
	
            
    --This functions returns an string in format xml that represents the report of test cases in xUnit
    --An example of valid report is:		
    --[[
    <?xml version="1.0" encoding="UTF-8"?>
    <testsuite name="nosetests" tests="1" errors="1" failures="0" skip="0">
        <testcase classname="path_to_test_suite.TestSomething" name="path_to_test_suite.TestSomething.test_it" time="0">
            <error type="exceptions.TypeError">
            Traceback (most recent call last):
            ...            
            TypeError: oops, wrong type
            </error>
        </testcase>
    </testsuite>
    ]]
    launcher.SaveReport = function (path)
        local xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
        
        local errors = 0
		local skip = 0
		local total = #tests
		
		local it = 1
		while tests[it] do
			if tests[it].result == nil then
				print("Skipped: "..tests[it].test);
				skip = skip + 1;
				table.remove(tests, it)
			else
				if tests[it].result ~= "OK" then
					errors = errors + 1
				end
				it = it + 1
			end
		end
        
        xml = xml..'<testsuite name="'..testSuiteName..'" tests="'..total..'" errors="'..errors..'" failures="0" skip="'..skip..'" date="'..os.date()..'">\n'
        
        for i = 1, #tests do
            xml = xml..'\t<testcase classname="'..tests[i].package.."."..tests[i].test..'" name="'..tests[i].description..'" time="'..(tests[i].time or "0")..'">'
            if tests[i].result ~= "OK" then
				xml = xml..'\n\t\t<error type="error"><![CDATA[\n'
				xml = xml .. format_error(tests[i].error)
				xml = xml..'\t\t]]></error>\n'
            end
            xml = xml..'\t</testcase>\n\n'
        end
        
        xml = xml..'</testsuite>\n'

        local file = io.open (path, "wb")
        file:write(xml)
        file:close()
    end
	
	
	return launcher
end


_G.TestLauncher = _M.TestLauncher
