local LibSerialize = require("LibSerialize")

local pairs = pairs
local type = type
local tostring = tostring
local assert = assert
local unpack = unpack
local pcall = pcall


--[[---------------------------------------------------------------------------
    Examples from the top of LibSerialize.lua
--]]---------------------------------------------------------------------------

do
    local t = { "test", [false] = {} }
    t[ t[false] ] = "hello"
    local serialized = LibSerialize:Serialize(t, "extra")
    local success, tab, str = LibSerialize:Deserialize(serialized)
    assert(success)
    assert(tab[1] == "test")
    assert(tab[ tab[false] ] == "hello")
    assert(str == "extra")
end

do
    local serialized = LibSerialize:SerializeEx(
        { errorOnUnserializableType = false },
        print, { a = 1, b = print })
    local success, fn, tab = LibSerialize:Deserialize(serialized)
    assert(success)
    assert(fn == nil)
    assert(tab.a == 1)
    assert(tab.b == nil)
end

do
    local t = { a = 1 }
    t.t = t
    t[t] = "test"
    local serialized = LibSerialize:Serialize(t)
    local success, tab = LibSerialize:Deserialize(serialized)
    assert(success)
    assert(tab.t.t.t.t.t.t.a == 1)
    assert(tab[tab.t] == "test")
end

do
    local t = { a = 1, b = print, c = 3 }
    local nested = { a = 1, b = print, c = 3 }
    t.nested = nested
    setmetatable(nested, { __LibSerialize = {
        filter = function(t, k, v) return k ~= "c" end
    }})
    local opts = {
        filter = function(t, k, v) return LibSerialize:IsSerializableType(k, v) end
    }
    local serialized = LibSerialize:SerializeEx(opts, t)
    local success, tab = LibSerialize:Deserialize(serialized)
    assert(success)
    assert(tab.a == 1)
    assert(tab.b == nil)
    assert(tab.c == 3)
    assert(tab.nested.a == 1)
    assert(tab.nested.b == nil)
    assert(tab.nested.c == nil)
end


--[[---------------------------------------------------------------------------
    Test of stable serialization
--]]---------------------------------------------------------------------------

do
    local t = { a = 1, b = print, c = 3 }
    local nested = { a = 1, b = print, c = 3 }
    t.nested = nested
    setmetatable(nested, { __LibSerialize = {
        filter = function(t, k, v) return k ~= "c" end
    }})
    local opts = {
        filter = function(t, k, v) return LibSerialize:IsSerializableType(k, v) end,
        stable = true
    }
    local serialized = LibSerialize:SerializeEx(opts, t)
    local success, tab = LibSerialize:Deserialize(serialized)
    assert(success)
    assert(tab.a == 1)
    assert(tab.b == nil)
    assert(tab.c == 3)
    assert(tab.nested.a == 1)
    assert(tab.nested.b == nil)
    assert(tab.nested.c == nil)
end

do
    local t1 = { x = "y", "test", [false] = { 1, 2, 3, a = "b" } }
    local opts = {
        stable = true,
        filter = function(t, k, v) return not tonumber(k) or tonumber(k) < 100 end
    }
    local serialized1 = LibSerialize:SerializeEx(opts, t1)
    local success1, tab1 = LibSerialize:Deserialize(serialized1)
    assert(success1)
    assert(tab1[1] == "test")
    assert(tab1.x == "y")
    assert(tab1[false][1] == 1)
    assert(tab1[false][2] == 2)
    assert(tab1[false][3] == 3)
    assert(tab1[false].a == "b")

    -- make a copy of the original table, but first insert a bunch of extra keys (which we'll
    -- filter out) to force the order of the hashes to be different (tested with lua 5.1 and 5.2)
    local t2 = {}
    for i = 100, 10000 do
        t2[tostring(i)] = i
    end
    t2.x = "y"
    t2[1] = "test"
    t2[false] = { 1, 2, 3, a = "b" }

    -- ensure the iteration order is different
    local isDifferent = false
    local k1, k2 = nil, nil
    while true do
        k1 = next(t1, k1)
        -- get the next key from t2 that's not going to be filtered
        while true do
            k2 = next(t2, k2)
            if k2 == nil or not tonumber(k2) or tonumber(k2) < 100 then
                break
            end
        end
        if k1 == nil and k2 == nil then
            break
        end
        assert(k1 ~= nil and k2 ~= nil)
        isDifferent = isDifferent or k1 ~= k2
    end
    assert(isDifferent)

    -- serialize the copy and ensure the result is the same
    local serialized2 = LibSerialize:SerializeEx(opts, t2)
    assert(serialized2 == serialized1)
end


--[[---------------------------------------------------------------------------
    Utilities
--]]---------------------------------------------------------------------------

local function tCompare(lhsTable, rhsTable, depth)
    depth = depth or 1
    for key, value in pairs(lhsTable) do
        if type(value) == "table" then
            local rhsValue = rhsTable[key]
            if type(rhsValue) ~= "table" then
                return false
            end
            if depth > 1 then
                if not tCompare(value, rhsValue, depth - 1) then
                    return false
                end
            end
        elseif value ~= rhsTable[key] then
            -- print("mismatched value: " .. key .. ": " .. tostring(value) .. ", " .. tostring(rhsTable[key]))
            return false
        end
    end
    -- Check for any keys that are in rhsTable and not lhsTable.
    for key, value in pairs(rhsTable) do
        if lhsTable[key] == nil then
            -- print("mismatched key: " .. key)
            return false
        end
    end
    return true
end


--[[---------------------------------------------------------------------------
    Test cases for serialization
--]]---------------------------------------------------------------------------

local function fail(value, desc)
    assert(false, ("Test failed (%s): %s"):format(tostring(value), desc))
end

local function testfilter(t, k, v)
    return k ~= "banned" and v ~= "banned"
end

local function check(value, bytelen, cmp)
    local serialized = LibSerialize:SerializeEx({ errorOnUnserializableType = false, filter = testfilter }, value)
    if #serialized ~= bytelen then
        fail(value, ("Unexpected serialized length (%d, expected %d)"):format(#serialized, bytelen))
    end

    local success, deserialized = LibSerialize:Deserialize(serialized)
    if not success then
        fail(value, ("Deserialization failed: %s"):format(deserialized))
    end

    local typ = type(value)
    if typ == "table" and not tCompare(cmp or value, deserialized) then
        fail(value, "Non-matching deserialization result")
    elseif typ ~= "table" and value ~= deserialized then
        fail(value, ("Non-matching deserialization result: %s"):format(tostring(deserialized)))
    end
end

-- Format: each test case is { value, bytelen, cmp }. The value will be serialized
-- and then deserialized, checking for success and equality, and the length of
-- the serialized string will be compared against bytelen. If `cmp` is provided,
-- it will be used for comparison against the deserialized result instead of `value`.
-- Note that the length always contains one extra byte for the version number.
local testCases = {
    { nil, 2 },
    { true, 2 },
    { false, 2 },
    { 0, 2 },
    { 1, 2 },
    { 127, 2 },
    { 128, 3 },
    { 4095, 3 },
    { 4096, 4 },
    { 65535, 4 },
    { 65536, 5 },
    { 16777215, 5 },
    { 16777216, 6 },
    { 4294967295, 6 },
    { 4294967296, 9 },
    { 9007199254740992, 9 },
    { 1.5, 6 },
    { 27.32, 8 },
    { 123.45678901235, 10 },
    { 148921291233.23, 10 },
    { -1, 3 },
    { -4095, 3 },
    { -4096, 4 },
    { -65535, 4 },
    { -65536, 5 },
    { -16777215, 5 },
    { -16777216, 6 },
    { -4294967295, 6 },
    { -4294967296, 9 },
    { -9007199254740992, 9 },
    { -1.5, 6 },
    { -123.45678901235, 10 },
    { -148921291233.23, 10 },
    { "", 2 },
    { "a", 3 },
    { "abcdefghijklmno", 17 },
    { "abcdefghijklmnop", 19 },
    { ("1234567890"):rep(30), 304 },
    { {}, 2 },
    { { 1 }, 3 },
    { { 1, 2, 3, 4, 5 }, 7 },
    { { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, 17 },
    { { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }, 19 },
    { { 1, 2, 3, 4, a = 1, b = 2, [true] = 3, d = 4 }, 17 },
    { { 1, 2, 3, 4, 5, a = 1, b = 2, c = true, d = 4 }, 21 },
    { { 1, 2, 3, 4, 5, a = 1, b = 2, c = 3, d = 4, e = false }, 24 },
    { { a = 1, b = 2, c = 3 }, 11 },
    { { "aa", "bb", "aa", "bb" }, 14 },
    { { "aa1", "bb2", "aa3", "bb4" }, 18 },
    { { "aa1", "bb2", "aa1", "bb2" }, 14 },
    { { "aa1", "bb2", "bb2", "aa1" }, 14 },
    { { "abcdefghijklmno", "abcdefghijklmno", "abcdefghijklmno", "abcdefghijklmno" }, 24 },
    { { "abcdefghijklmno", "abcdefghijklmno", "abcdefghijklmno", "abcdefghijklmnop" }, 40 },
    { { 1, 2, 3, print, print, 6 }, 7, { 1, 2, 3, nil, nil, 6 } },
    { { 1, 2, 3, print, 5, 6 }, 8, { 1, 2, 3, nil, 5, 6 } },
    { { a = print, b = 1, c = print }, 5, { b = 1 } },
    { { a = print, [print] = "a" }, 2, {} },
    { { "banned", 1, 2, 3, banned = 4, test = "banned", a = 1 }, 9, { nil, 1, 2, 3, a = 1 } },
}

do
    local t = { a = 1, b = 2 }
    table.insert(testCases, { { t, t, t }, 13 })
    table.insert(testCases, { { { a = 1, b = 2 }, { a = 1, b = 2 }, { a = 1, b = 2 } }, 23 })
end

for _, testCase in ipairs(testCases) do
    check(unpack(testCase))
end

-- Since all the above tests assume serialization success, try some failures now.
local failCases = {
    { print },
    { [print] = true },
    { [true] = print },
    print,
}

for _, testCase in ipairs(failCases) do
    local success = pcall(LibSerialize.Serialize, LibSerialize, testCase)
    assert(success == false)
end

print("All tests passed!")
