require "cjson"

-- Misc routines to assist with CJSON testing
--
-- Mark Pulford <mark@kyne.com.au>

-- Determine with a Lua table can be treated as an array.
-- Explicitly returns "not an array" for very sparse arrays.
-- Returns:
-- -1   Not an array
-- 0    Empty table
-- >0   Highest index in the array
function is_array(table)
    local max = 0
    local count = 0
    for k, v in pairs(table) do
        if type(k) == "number" then
            if k > max then max = k end
            count = count + 1
        else
            return -1
        end
    end
    if max > count * 2 then
        return -1
    end

    return max
end

function serialise_table(value, indent, depth)
    local spacing, spacing2, indent2
    if indent then
        spacing = "\n" .. indent
        spacing2 = spacing .. "  "
        indent2 = indent .. "  "
    else
        spacing, spacing2, indent2 = " ", " ", false
    end
    depth = depth + 1
    if depth > 50 then
        return "Cannot serialise any further: too many nested tables"
    end

    local max = is_array(value)

    local comma = false
    local fragment = { "{" .. spacing2 }
    if max > 0 then
        -- Serialise array
        for i = 1, max do
            if comma then
                table.insert(fragment, "," .. spacing2)
            end
            table.insert(fragment, serialise_value(value[i], indent2, depth))
            comma = true
        end
    elseif max < 0 then
        -- Serialise table
        for k, v in pairs(value) do
            if comma then
                table.insert(fragment, "," .. spacing2)
            end
            table.insert(fragment, string.format(
                "[%s] = %s", serialise_value(k, indent2, depth),
                             serialise_value(v, indent2, depth))
            )
            comma = true
        end
    end
    table.insert(fragment, spacing .. "}")

    return table.concat(fragment)
end

function serialise_value(value, indent, depth)
    if indent == nil then indent = "" end
    if depth == nil then depth = 0 end

    if value == cjson.null then
        return "cjson.null"
    elseif type(value) == "string" then
        return string.format("%q", value)
    elseif type(value) == "nil" or type(value) == "number" or
           type(value) == "boolean" then
        return tostring(value)
    elseif type(value) == "table" then
        return serialise_table(value, indent, depth)
    else
        return "\"<" .. type(value) .. ">\""
    end
end

function dump_value(value)
    print(serialise_value(value))
end

function file_load(filename)
    local file
    if filename == nil then
        file = io.stdin
    else
        local err
        file, err = io.open(filename)
        if file == nil then
            error(string.format("Unable to read '%s': %s", filename, err))
        end
    end
    local data = file:read("*a")

    if filename ~= nil then
        file:close()
    end

    if data == nil then
        error("Failed to read " .. filename)
    end

    return data
end

function file_save(filename, data)
    local file
    if filename == nil then
        file = io.stdout
    else
        local err
        file, err = io.open(filename, "w")
        if file == nil then
            error(string.format("Unable to write '%s': %s", filename, err))
        end
    end
    file:write(data)
    if filename ~= nil then
        file:close()
    end
end

function compare_values(val1, val2)
    local type1 = type(val1)
    local type2 = type(val2)
    if type1 ~= type2 then
        return false
    end

    -- Check for NaN
    if type1 == "number" and val1 ~= val1 and val2 ~= val2 then
        return true
    end

    if type1 ~= "table" then
        return val1 == val2
    end

    -- check_keys stores all the keys that must be checked in val2
    local check_keys = {}
    for k, _ in pairs(val1) do
        check_keys[k] = true
    end

    for k, v in pairs(val2) do
        if not check_keys[k] then
            return false
        end

        if not compare_values(val1[k], val2[k]) then
            return false
        end

        check_keys[k] = nil
    end
    for k, _ in pairs(check_keys) do
        -- Not the same if any keys from val1 were not found in val2
        return false
    end
    return true
end

function run_test(testname, func, input, should_work, output)
    local function status_line(name, status, value)
        local statusmap = { [true] = ":success", [false] = ":error" }
        if status ~= nil then
            name = name .. statusmap[status]
        end
        print(string.format("[%s] %s", name, serialise_value(value, false)))
    end

    local result = { pcall(func, unpack(input)) }
    local success = table.remove(result, 1)

    local correct = false
    if success == should_work and compare_values(result, output) then
        correct = true
    end

    local teststatus = { [true] = "PASS", [false] = "FAIL" }
    print("==> Test " .. testname .. ": " .. teststatus[correct])

    status_line("Input", nil, input)
    if not correct then
        status_line("Expected", should_work, output)
    end
    status_line("Received", success, result)
    print()

    return correct, result
end

function run_test_group(testgroup, tests)
    local function run_config(configname, func)
        local success, msg = pcall(func)
        if msg then
            print(string.format("==> Config %s: %s", configname, msg))
        end
        print()
    end

    local function test_id(group, id)
        return string.format("%s [%d]", group, id)
    end

    for k, v in ipairs(tests) do
        if type(v) == "function" then
            -- Useful for changing configuration during a batch
            run_config(test_id(testgroup, k), v)
        elseif type(v) == "table" then
            run_test(test_id(testgroup, k), unpack(v))
        else
            error("Testgroup can only contain functions and tables")
        end
    end
end

-- vi:ai et sw=4 ts=4:
