local lpeg = require("lpeg")
local json = require("json")
local lunit = require("lunit")
local math = require("math")
local testutil = require("testutil")

local encode = json.encode
-- DECODE NOT 'local' due to requirement for testutil to access it
decode = json.decode.getDecoder(false)

if not module then
    _ENV = lunit.module("lunit-calls", 'seeall')
else
    module("lunit-calls", lunit.testcase, package.seeall)
end

function setup()
	-- Ensure that the decoder is reset
	_G["decode"] = json.decode.getDecoder(false)
end

local values = {
	0,
	1,
	0.2,
	"Hello",
	true,
	{hi=true},
	{1,2}
}

function test_identity()
	local function testFunction(capturedName, ...)
		assert_equal('call', capturedName)
		return (...)
	end
	local strict = {
		calls = { defs = {
			call = testFunction
		} }
	}
	local decode = json.decode.getDecoder(strict)
	for i, v in ipairs(values) do
		local str = "call(" .. encode(v) .. ")"
		local decoded = decode(str)
		if type(decoded) == 'table' then
			for k2, v2 in pairs(v) do
				assert_equal(v2, decoded[k2])
				decoded[k2] = nil
			end
			assert_nil(next(decoded))
		else
			assert_equal(v, decoded)
		end
	end
end

-- Test for a function that throws
function test_function_failure()
	local function testFunction(...)
		error("CANNOT CONTINUE")
	end
	local strict = {
		calls = { defs = {
			call = testFunction
		} }
	}
	local decode = json.decode.getDecoder(strict)
	for i, v in ipairs(values) do
		local str = "call(" .. encode(v) .. ")"
		assert_error(function()
			decode(str)
		end)
	end
end

-- Test for a function that is not a function
function test_not_a_function_fail()
	local notFunction = {
		0/0, 1/0, -1/0, 0, 1, "Hello", {}, coroutine.create(function() end)
	}
	for _, v in ipairs(notFunction) do
		assert_error(function()
			local strict = {
				calls = { defs = {
					call = v
				}, allowUndefined = false }
			}
			json.decode.getDecoder(strict)
		end)
	end
end

function test_not_permitted_fail()
	local strict = {
		calls = {
			defs = { call = false }
		}
	}
	local decoder = json.decode.getDecoder(strict)
	assert_error(function()
		decoder("call(1)")
	end)
end

function test_permitted()
	local strict = {
		calls = {
			defs = { call = true, other = true }
		}
	}
	local decoder = json.decode.getDecoder(strict)
	assert(decoder("call(1)").name == 'call')
	assert(decoder("other(1)").name == 'other')
end

function test_permitted_trailing()
	local strict = {
		calls = {
			defs = { call = true, other = true }
		}
	}
	local decoder = json.decode.getDecoder(strict)
	assert(decoder("call(1,)").name == 'call')
	assert(decoder("other(1,)").name == 'other')
end
function test_permitted_no_trailing()
	local strict = {
		calls = {
			defs = { call = true, other = true },
			trailingComma = false
		}
	}
	local decoder = json.decode.getDecoder(strict)
	assert_error(function()
		decoder("call(1,)")
	end)
	assert_error(function()
		decoder("other(1,)")
	end)
end
function test_permitted_nested()
	local strict = {
		calls = {
			defs = { call = true, other = true }
		}
	}
	local decoder = json.decode.getDecoder(strict)
	assert(decoder("call(call(1))").name == 'call')
	assert(decoder("other(call(1))").name == 'other')
end

function test_not_defined_fail()
	local decoder = json.decode.getDecoder({
		calls = {
			allowUndefined = false
		}
	})
	assert_error(function()
		decoder("call(1)")
	end)
end

function test_not_defined_succeed()
	local decoder = json.decode.getDecoder({
		calls = {
			allowUndefined = true
		}
	})
	assert(decoder("call(1)").name == 'call')
end

-- Test for a name that is not a string
function test_name_not_string()
	local notString = {
		true, false, 0/0, 1/0, -1/0, 0, 1, {}, function() end, coroutine.create(function() end)
	}
	for _, v in ipairs(notString) do
		assert_error(function()
			local defs = {
				[v] = function() end
			}
			local strict = {
				calls = { defs = defs }
			}
			json.decode.getDecoder(strict)
		end)
	end
end

-- Test for a name that is a string or a pattern
function test_name_matches_string_or_pattern()
	local matchedValues = {
		["mystring"] = "mystring",
		[lpeg.C(lpeg.P("m") * (lpeg.P("y") + lpeg.P("Y")) * "string")] = "mystring",
		[lpeg.C(lpeg.P("m") * (lpeg.P("y") + lpeg.P("Y")) * "string")] = "mYstring"
	}
	for pattern, value in pairs(matchedValues) do
		local matched = false
		local function mustBeCalled(capturedName, ...)
			assert_equal(value, capturedName)
			matched = true
		end
		matched = false
		local strict = {
			calls = { defs = {
				[pattern] = mustBeCalled
			} }
		}
		json.decode.getDecoder(strict)(value .. "(true)")
		assert_true(matched, "Value <" .. value .. "> did not match the given pattern")
	end
end
