--:Minify:--

local passed, failed = 0, 0

local function pass(msg) syscall.devctl(1,"sfgc",10); print("  PASS  "..msg); syscall.devctl(1,"sfgc",1) end
local function fail(msg) syscall.devctl(1,"sfgc",2);  print("  FAIL  "..msg); syscall.devctl(1,"sfgc",1) end
local function info(msg) syscall.devctl(1,"sfgc",14); print("  ....  "..msg); syscall.devctl(1,"sfgc",1) end
local function head(msg) syscall.devctl(1,"sfgc",4);  print("\n"..msg);       syscall.devctl(1,"sfgc",1) end

local function check(label, ok, err)
    if ok then passed = passed + 1; pass(label)
    else       failed = failed + 1; fail(label.." - "..tostring(err)) end
end

local function writeFile(path, data)
    local ok, fd = pcall(syscall.open, path, "w")
    if not ok then return false, fd end
    local ok2, err = pcall(syscall.write, fd, data)
    pcall(syscall.close, fd)
    return ok2, err
end

local function readFile(path)
    local ok, fd = pcall(syscall.open, path, "r")
    if not ok then return false, fd end
    local ok2, data = pcall(syscall.read, fd, 65536)
    pcall(syscall.close, fd)
    return ok2, data
end

local function rmrf(path)
    local t = syscall.type(path)
    if t == "directory" then
        local ok, entries = pcall(syscall.listdir, path)
        if ok then
            for _, name in ipairs(entries) do
                rmrf(path:gsub("/$","").."/"..name)
            end
        end
        pcall(syscall.remove, path)
    elseif t == "file" then
        pcall(syscall.remove, path)
    end
end

local SCRATCH    = "/tmp/looptest_scratch"
local SRC_DIR    = SCRATCH.."/src"
local BIND_MNT   = SCRATCH.."/bind_mnt"
local IMG_PATH   = SCRATCH.."/test.hfs"
local IMG_MNT    = SCRATCH.."/img_mnt"
local BIND_LOOP  = nil
local IMG_LOOP   = nil

rmrf(SCRATCH)
pcall(syscall.mkdir, SCRATCH)
pcall(syscall.mkdir, SRC_DIR)
pcall(syscall.mkdir, BIND_MNT)
pcall(syscall.mkdir, IMG_MNT)
pcall(syscall.mkdir, SRC_DIR.."/subdir")

writeFile(SRC_DIR.."/hello.txt",       "hello from hyperion\n")
writeFile(SRC_DIR.."/data.txt",        "line1\nline2\nline3\n")
writeFile(SRC_DIR.."/subdir/deep.txt", "deep file\n")

head("[ 1 ] bind mode - losetup on a directory")
do
    local ok, id = pcall(syscall.losetup, SRC_DIR)
    check("losetup(dir) returns a loop id", ok and type(id) == "string" and id:sub(1,4) == "loop", id)
    if ok then
        BIND_LOOP = id
        info("attached as "..id)
    end
end

head("[ 2 ] bind mode - mount and read files")
do
    if BIND_LOOP then
        local mok = pcall(syscall.mount, BIND_MNT, BIND_LOOP)
        check("mount(bind_mnt, "..BIND_LOOP..")", mok, "mount failed")

        if mok then
            local lok, entries = pcall(syscall.listdir, BIND_MNT)
            check("listdir through bind mount", lok and type(entries) == "table", entries)

            local rok, data = readFile(BIND_MNT.."/hello.txt")
            check("read hello.txt through bind", rok and data == "hello from hyperion\n",
                  rok and ("got: "..tostring(data):sub(1,40)) or tostring(data))

            local rok2, data2 = readFile(BIND_MNT.."/subdir/deep.txt")
            check("read subdir/deep.txt through bind", rok2 and data2 == "deep file\n",
                  rok2 and ("got: "..tostring(data2)) or tostring(data2))
        end
    else
        check("mount (skipped - no loop id)", false, "losetup failed in [1]")
    end
end

head("[ 3 ] bind mode - write through loop mount, verify on host")
do
    if BIND_LOOP then
        local wok, werr = writeFile(BIND_MNT.."/written.txt", "written via loop\n")
        check("write new file through bind mount", wok, werr)

        local rok, data = readFile(BIND_MNT.."/written.txt")
        check("read back through bind mount", rok and data == "written via loop\n",
              rok and ("got: "..tostring(data)) or tostring(data))

        local rok2, data2 = readFile(SRC_DIR.."/written.txt")
        check("file visible on host path (bind is transparent)", rok2 and data2 == "written via loop\n",
              rok2 and ("got: "..tostring(data2)) or tostring(data2))
    else
        check("write (skipped)", false, "bind mount not set up")
    end
end

head("[ 4 ] bind mode - lodetach while mounted returns EBUSY")
do
    if BIND_LOOP then
        local ok = pcall(syscall.lodetach, BIND_LOOP)
        check("lodetach while mounted is refused (EBUSY)", not ok, "should have errored")
    else
        check("lodetach busy check (skipped)", false, "no bind loop")
    end
end

head("[ 5 ] bind mode - umount then lodetach")
do
    if BIND_LOOP then
        local uok = pcall(syscall.umount, BIND_MNT)
        check("umount(bind_mnt)", uok, "umount failed")

        local dok = pcall(syscall.lodetach, BIND_LOOP)
        check("lodetach after umount", dok, "lodetach failed")

        if dok then BIND_LOOP = nil end
    else
        check("umount+lodetach (skipped)", false, "no bind loop")
    end
end

head("[ 6 ] loimgcreate - serialise directory to HFS image")
do
    local ok, imgStr = pcall(syscall.loimgcreate, SRC_DIR)
    check("loimgcreate(srcdir) returns a string", ok and type(imgStr) == "string" and #imgStr > 0, imgStr)

    if ok then
        info("image size: "..#imgStr.." bytes")

        local isBHFS = imgStr:sub(1, 4) == "BHFS"
        check("image has BHFS magic header", isBHFS,
              "got: "..imgStr:sub(1,4):gsub(".", function(c) return string.format("%02X ", c:byte()) end))
        check("image has correct version byte (0x01)", imgStr:byte(5) == 1,
              "version byte: "..tostring(imgStr:byte(5)))
        check("image contains FILE record (type=0x01)", imgStr:find("\001", 9, true) ~= nil, "no FILE type byte found")
        check("image contains DIR record (type=0x02)",  imgStr:find("\002", 9, true) ~= nil, "no DIR type byte found")
        check("image ends with END record (type=0xFF)", imgStr:byte(#imgStr) == 0xFF,
              "last byte: 0x"..string.format("%02X", imgStr:byte(#imgStr)))

        local wok, werr = pcall(syscall.loimgwrite, imgStr, IMG_PATH)
        check("loimgwrite writes image file", wok, werr)
        check("image file exists on disk",   syscall.type(IMG_PATH) == "file", "file not found")
    end
end

head("[ 7 ] HFS image - losetup attaches image file")
do
    if syscall.type(IMG_PATH) == "file" then
        local ok, id = pcall(syscall.losetup, IMG_PATH)
        check("losetup(image.hfs) returns loop id", ok and type(id) == "string", id)
        if ok then
            IMG_LOOP = id
            info("image attached as "..id)

            local lok, devs = pcall(syscall.lolist)
            local found = false
            if lok then
                for lid, info_entry in pairs(devs) do
                    if lid == id then found = true end
                end
            end
            check("lolist() contains new image device", found, "not found in lolist")
        end
    else
        check("losetup image (skipped - no image file)", false, "image not created in [6]")
    end
end

head("[ 8 ] HFS image - mount and read files")
do
    if IMG_LOOP then
        local mok = pcall(syscall.mount, IMG_MNT, IMG_LOOP)
        check("mount(img_mnt, "..IMG_LOOP..")", mok, "mount failed")

        if mok then
            local lok, entries = pcall(syscall.listdir, IMG_MNT)
            check("listdir through image mount returns table", lok and type(entries) == "table", entries)
            if lok then
                local hasHello = false
                local hasSubdir = false
                for _, e in ipairs(entries) do
                    if e == "hello.txt" then hasHello = true end
                    if e == "subdir"    then hasSubdir = true end
                end
                check("hello.txt visible in image root",   hasHello,  "not listed")
                check("subdir/ visible in image root",     hasSubdir, "not listed")
            end

            local rok, data = readFile(IMG_MNT.."/hello.txt")
            check("read hello.txt from image", rok and data == "hello from hyperion\n",
                  rok and ("got: "..tostring(data):sub(1,40)) or tostring(data))

            local rok2, data2 = readFile(IMG_MNT.."/data.txt")
            check("read data.txt from image", rok2 and data2 == "line1\nline2\nline3\n",
                  rok2 and ("got: "..tostring(data2)) or tostring(data2))
        end
    else
        check("image mount read (skipped)", false, "no image loop")
    end
end

head("[ 9 ] HFS image - write new files into image mount")
do
    if IMG_LOOP then
        local wok, werr = writeFile(IMG_MNT.."/newfile.txt", "created inside image\n")
        check("write new file into image mount", wok, werr)

        local rok, data = readFile(IMG_MNT.."/newfile.txt")
        check("read back newly written file", rok and data == "created inside image\n",
              rok and ("got: "..tostring(data)) or tostring(data))

        local wok2, werr2 = writeFile(IMG_MNT.."/hello.txt", "overwritten\n")
        check("overwrite existing file in image", wok2, werr2)

        local rok2, data2 = readFile(IMG_MNT.."/hello.txt")
        check("overwritten content reads back correctly", rok2 and data2 == "overwritten\n",
              rok2 and ("got: "..tostring(data2)) or tostring(data2))

        local rok3, orig = readFile(IMG_PATH)
        check("disk image file is unchanged after in-memory write",
              rok3 and orig and orig:find("/hello%.txt") ~= nil,
              rok3 and "filename record missing from image" or tostring(orig))
    else
        check("image write test (skipped)", false, "image not mounted")
    end
end

head("[ 10 ] HFS image - sub-directory traversal")
do
    if IMG_LOOP then
        local t = syscall.type(IMG_MNT.."/subdir")
        check("type(subdir) == 'directory'", t == "directory", "got: "..tostring(t))

        local lok, entries = pcall(syscall.listdir, IMG_MNT.."/subdir")
        check("listdir(subdir) works", lok and type(entries) == "table", entries)

        local rok, data = readFile(IMG_MNT.."/subdir/deep.txt")
        check("read subdir/deep.txt from image", rok and data == "deep file\n",
              rok and ("got: "..tostring(data)) or tostring(data))

        local mok = pcall(syscall.mkdir, IMG_MNT.."/subdir/newdir")
        check("mkdir inside image mount", mok, "mkdir failed")
        check("new dir has type 'directory'",
              syscall.type(IMG_MNT.."/subdir/newdir") == "directory",
              "wrong type")

        local wok, werr = writeFile(IMG_MNT.."/subdir/newdir/x.txt", "x\n")
        check("write file in newly created subdir", wok, werr)
    else
        check("subdir traversal (skipped)", false, "image not mounted")
    end
end

head("[ 11 ] HFS image - lodetach while mounted returns EBUSY")
do
    if IMG_LOOP then
        local ok = pcall(syscall.lodetach, IMG_LOOP)
        check("lodetach image while mounted is refused", not ok, "should have errored EBUSY")
    else
        check("lodetach busy (skipped)", false, "no image loop")
    end
end

head("[ 12 ] HFS image - umount then lodetach")
do
    if IMG_LOOP then
        local uok = pcall(syscall.umount, IMG_MNT)
        check("umount(img_mnt)", uok, "umount failed")

        local dok = pcall(syscall.lodetach, IMG_LOOP)
        check("lodetach after umount", dok, "lodetach failed")

        if dok then
            local lok, devs = pcall(syscall.lolist)
            local found = false
            if lok then
                for lid in pairs(devs) do
                    if lid == IMG_LOOP then found = true end
                end
            end
            check("lolist no longer shows detached device", not found, "still present in lolist")
            IMG_LOOP = nil
        end
    else
        check("image umount+lodetach (skipped)", false, "no image loop")
    end
end

head("[ 13 ] lolist - reflects attached device count")
do
    local ok1, id1 = pcall(syscall.losetup, SRC_DIR)
    local ok2, id2 = pcall(syscall.losetup, SRC_DIR)
    check("attach first device for lolist test",  ok1, id1)
    check("attach second device for lolist test", ok2, id2)

    if ok1 and ok2 then
        local lok, devs = pcall(syscall.lolist)
        check("lolist() succeeds", lok, devs)
        if lok then
            local found1, found2 = false, false
            for lid in pairs(devs) do
                if lid == id1 then found1 = true end
                if lid == id2 then found2 = true end
            end
            check("lolist contains first device",  found1, "missing "..id1)
            check("lolist contains second device", found2, "missing "..id2)
            local count = 0
            for _ in pairs(devs) do count = count + 1 end
            info("lolist shows "..count.." device(s)")
        end
    end

    if ok1 then pcall(syscall.lodetach, id1) end
    if ok2 then pcall(syscall.lodetach, id2) end
end

head("[ 14 ] losetup - non-existent path returns error")
do
    local ok = pcall(syscall.losetup, "/tmp/does_not_exist_xyz_looptest")
    check("losetup on missing path errors", not ok, "should have errored")
end

head("[ 15 ] mount - same loop device cannot be mounted twice")
do
    local ok, id = pcall(syscall.losetup, SRC_DIR)
    check("losetup for double-mount test", ok, id)
    if ok then
        local m1ok = pcall(syscall.mount, BIND_MNT, id)
        check("first mount succeeds", m1ok, "mount failed")

        if m1ok then
            local m2ok = pcall(syscall.mount, IMG_MNT, id)
            check("second mount of same device is refused", not m2ok, "should have errored EBUSY")
            pcall(syscall.umount, BIND_MNT)
        end
        pcall(syscall.lodetach, id)
    end
end

head("[ 16 ] loimgcreate - on a regular file returns ENOTDIR")
do
    local ok = pcall(syscall.loimgcreate, IMG_PATH)
    check("loimgcreate on a file errors (ENOTDIR)", not ok, "should have errored")
end

head("[ 17 ] HFS image - binary round-trip (all byte values)")
do
    local bytes = {}
    for i = 0, 255 do bytes[i+1] = string.char(i) end
    local binData = table.concat(bytes)

    local binSrc = SCRATCH.."/binsrc"
    pcall(syscall.mkdir, binSrc)
    writeFile(binSrc.."/binary.bin", binData)

    local ok1, imgStr = pcall(syscall.loimgcreate, binSrc)
    check("loimgcreate handles binary content", ok1, imgStr)

    if ok1 then
        local binImg = SCRATCH.."/binary.hfs"
        pcall(syscall.loimgwrite, imgStr, binImg)

        local ok2, lid = pcall(syscall.losetup, binImg)
        check("losetup on binary image", ok2, lid)

        if ok2 then
            local mnt = SCRATCH.."/binmnt"
            pcall(syscall.mkdir, mnt)
            local mok = pcall(syscall.mount, mnt, lid)
            check("mount binary image", mok, "mount failed")

            if mok then
                local rok, readBack = readFile(mnt.."/binary.bin")
                check("binary file readable from image", rok, readBack)
                check("binary data round-trips without corruption",
                      rok and readBack == binData,
                      rok and string.format("length in=%d out=%d", #binData, #(readBack or "")) or tostring(readBack))
                pcall(syscall.umount, mnt)
            end
            pcall(syscall.lodetach, lid)
        end
    end
end

head("[ 18 ] second-run safety - lolist is empty after full cleanup")
do
    local lok, devs = pcall(syscall.lolist)
    check("lolist() call succeeds", lok, devs)
    if lok then
        local count = 0
        for _ in pairs(devs) do count = count + 1 end
        check("no leftover loop devices after all tests", count == 0,
              count.." device(s) still attached: "..
              (function()
                  local ids = {}
                  for id in pairs(devs) do ids[#ids+1] = id end
                  return table.concat(ids, ", ")
              end)())
    end
end

rmrf(SCRATCH)

print("")
syscall.devctl(1, "sfgc", failed == 0 and 10 or 2)
print(string.format("Results: %d passed, %d failed", passed, failed))
syscall.devctl(1, "sfgc", 1)
if failed > 0 then syscall.exit(1) end
