-- SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0

--- Tests the io library is (mostly) consistent with PUC Lua.
--
-- These tests are based on the tests for Lua 5.1 and 5.3

describe("The io library", function()
    local file = "/test-files/tmp.txt"
    local otherfile = "/test-files/tmp2.txt"

    local t = '0123456789'
    for _ = 1, 12 do t = t .. t end
    assert(#t == 10 * 2 ^ 12)

    local function read_all(f)
        local h = fs.open(f, "rb")
        local contents = h.readAll()
        h.close()
        return contents
    end

    local function write_file(f, contents)
        local h = fs.open(f, "wb")
        h.write(contents)
        h.close()
    end

    local function setup()
        write_file(file, "\"\225lo\"{a}\nsecond line\nthird line \n\225fourth_line\n\n\9\9  3450\n")
    end

    describe("io.close", function()
        it("cannot close stdin", function()
            expect{ io.stdin:close() }:same { nil, "attempt to close standard stream" }
        end)
        it("cannot close stdout", function()
            expect{ io.stdout:close() }:same { nil, "attempt to close standard stream" }
        end)
        it("cannot close stdout", function()
            expect{ io.stdout:close() }:same { nil, "attempt to close standard stream" }
        end)
    end)

    it("io.input on a handle returns that handle", function()
        expect(io.input(io.stdin)):equals(io.stdin)
    end)

    it("io.output on a handle returns that handle", function()
        expect(io.output(io.stdout)):equals(io.stdout)
    end)

    it("defines a __name field", function()
        expect(getmetatable(io.input()).__name):eq("FILE*")
    end)

    describe("io.type", function()
        it("returns file on handles", function()
            local handle = io.input()
            expect(handle):type("table")
            expect(io.type(handle)):equals("file")
            expect(io.type(io.stdin)):equals("file")
        end)

        it("returns nil on values", function()
            expect(io.type(8)):equals(nil)
        end)

        it("returns nil on tables", function()
            expect(io.type(setmetatable({}, {}))):equals(nil)
        end)
    end)

    describe("io.lines", function()
        it("validates arguments", function()
            io.lines(nil)
            expect.error(io.lines, ""):eq("/: Not a file")
            expect.error(io.lines, false):eq("bad argument #1 (string expected, got boolean)")
        end)

        it("closes the file", function()
            setup()

            local n = 0
            local f = io.lines(file)
            while f() do n = n + 1 end
            expect(n):eq(6)

            expect.error(f):eq("file is already closed")
            expect.error(f):eq("file is already closed")
        end)

        it("can copy a file", function()
            setup()

            local n = 0
            io.output(otherfile)
            for l in io.lines(file) do
                io.write(l, "\n")
                n = n + 1
            end
            io.close()
            expect(n):eq(6)

            io.input(file)
            local f = io.open(otherfile):lines()
            local n = 0
            for l in io.lines() do
                expect(l):eq(f())
                n = n + 1
            end
            expect(n):eq(6)
        end)

        it("does not close on a normal file handle", function()
            setup()

            local f = assert(io.open(file))
            local n = 0
            for _ in f:lines() do n = n + 1 end
            expect(n):eq(6)

            expect(tostring(f):sub(1, 5)):eq("file ")
            assert(f:close())

            expect(tostring(f)):eq("file (closed)")
            expect(io.type(f)):eq("closed file")
        end)

        it("accepts multiple arguments", function()
            write_file(file, "0123456789\n")
            for a, b in io.lines(file, 1, 1) do
                if a == "\n" then
                    expect(b):eq(nil)
                else
                    expect(tonumber(a)):eq(tonumber(b) - 1)
                end
            end

            for a, b, c in io.lines(file, 1, 2, "a") do
                expect(a):eq("0")
                expect(b):eq("12")
                expect(c):eq("3456789\n")
            end

            for a, b, c in io.lines(file, "a", 0, 1) do
                if a == "" then break end
                expect(a):eq("0123456789\n")
                expect(b):eq(nil)
                expect(c):eq(nil)
            end

            write_file(file, "00\n10\n20\n30\n40\n")
            for a, b in io.lines(file, "n", "n") do
                if a == 40 then
                    expect(b):eq(nil)
                else
                    expect(a):eq(b - 10)
                end
            end
        end)
    end)

    describe("io.open", function()
        it("validates arguments", function()
            io.open("")
            io.open("", "r")

            expect.error(io.open, nil):eq("bad argument #1 (string expected, got nil)")
            expect.error(io.open, "", false):eq("bad argument #2 (string expected, got boolean)")
        end)

        it("checks the mode", function()
            io.open(file, "w"):close()

            -- This really should be invalid mode, but I'll live.
            expect.error(io.open, file, "rw"):str_match("Unsupported mode")
            -- TODO: expect.error(io.open, file, "rb+"):str_match("Unsupported mode")
            expect.error(io.open, file, "r+bk"):str_match("Unsupported mode")
            expect.error(io.open, file, ""):str_match("Unsupported mode")
            expect.error(io.open, file, "+"):str_match("Unsupported mode")
            expect.error(io.open, file, "b"):str_match("Unsupported mode")

            assert(io.open(file, "r+b")):close()
            assert(io.open(file, "r+")):close()
            assert(io.open(file, "rb")):close()
        end)

        it("returns an error message on non-existent files", function()
            local a, b = io.open('xuxu_nao_existe')
            expect(a):equals(nil)
            expect(b):type("string")
        end)
    end)

    describe("a readable handle", function()
        it("cannot be written to", function()
            write_file(file, "")
            io.input(file)
            expect { io.input():write("xuxu") }:same { nil, "file is not writable" }
            io.input(io.stdin)
        end)

        it("supports various modes", function()
            write_file(file, "alo\n " .. t .. " ;end of file\n")

            io.input(file)
            expect(io.read()):eq("alo")
            expect(io.read(1)):eq(' ')
            expect(io.read(#t)):eq(t)
            expect(io.read(1)):eq(' ')
            expect(io.read(0))
            expect(io.read('*a')):eq(';end of file\n')
            expect(io.read(0)):eq(nil)
            expect(io.close(io.input())):eq(true)

            fs.delete(file)
        end)

        it("support seeking", function()
            setup()
            io.input(file)

            expect(io.read(0)):eq("")   -- not eof
            expect(io.read(5, '*l')):eq('"\225lo"')
            expect(io.read(0)):eq("")
            expect(io.read()):eq("second line")
            local x = io.input():seek()
            expect(io.read()):eq("third line ")
            assert(io.input():seek("set", x))
            expect(io.read('*l')):eq("third line ")
            expect(io.read(1)):eq("\225")
            expect(io.read(#"fourth_line")):eq("fourth_line")
            assert(io.input():seek("cur", -#"fourth_line"))
            expect(io.read()):eq("fourth_line")
            expect(io.read()):eq("")  -- empty line
            expect(io.read(8)):eq('\9\9  3450') -- FIXME: Not actually supported
            expect(io.read(1)):eq('\n')
            expect(io.read(0)):eq(nil)  -- end of file
            expect(io.read(1)):eq(nil)  -- end of file
            expect(({ io.read(1) })[2]):eq(nil)
            expect(io.read()):eq(nil)  -- end of file
            expect(({ io.read() })[2]):eq(nil)
            expect(io.read('*n')):eq(nil)  -- end of file
            expect(({ io.read('*n') })[2]):eq(nil)
            expect(io.read('*a')):eq('')  -- end of file (OK for `*a')
            expect(io.read('*a')):eq('')  -- end of file (OK for `*a')

            io.close(io.input())
        end)

        it("supports the 'L' mode", function()
            write_file(file, "\n\nline\nother")

            io.input(file)
            expect(io.read"L"):eq("\n")
            expect(io.read"L"):eq("\n")
            expect(io.read"L"):eq("line\n")
            expect(io.read"L"):eq("other")
            expect(io.read"L"):eq(nil)
            io.input():close()

            local f = assert(io.open(file))
            local s = ""
            for l in f:lines("L") do s = s .. l end
            expect(s):eq("\n\nline\nother")
            f:close()

            io.input(file)
            s = ""
            for l in io.lines(nil, "L") do s = s .. l end
            expect(s):eq("\n\nline\nother")
            io.input():close()

            s = ""
            for l in io.lines(file, "L") do s = s .. l end
            expect(s):eq("\n\nline\nother")

            s = ""
            for l in io.lines(file, "l") do s = s .. l end
            expect(s):eq("lineother")

            write_file(file, "a = 10 + 34\na = 2*a\na = -a\n")
            local t = {}
            load(io.lines(file, "L"), nil, nil, t)()
            expect(t.a):eq(-((10 + 34) * 2))
        end)
    end)

    describe("a writable handle", function()
        it("supports seeking", function()
            fs.delete(file)
            io.output(file)

            expect(io.output()):not_equals(io.stdout)

            expect(io.output():seek()):equal(0)
            assert(io.write("alo alo"))
            expect(io.output():seek()):equal(#"alo alo")
            expect(io.output():seek("cur", -3)):equal(#"alo alo" - 3)
            assert(io.write("joao"))
            expect(io.output():seek("end")):equal(#"alo joao")

            expect(io.output():seek("set")):equal(0)

            assert(io.write('"\225lo"', "{a}\n", "second line\n", "third line \n"))
            assert(io.write('\225fourth_line'))

            io.output(io.stdout)
            expect(io.output()):equals(io.stdout)
        end)

        it("supports appending", function()
            io.output(file)
            io.write("alo\n")
            io.close()
            expect.error(io.write)

            local f = io.open(file, "a")
            io.output(f)

            assert(io.write(' ' .. t .. ' '))
            assert(io.write(';', 'end of file\n'))
            f:flush()
            io.flush()
            f:close()

            expect(read_all(file)):eq("alo\n " .. t .. " ;end of file\n")
        end)
    end)

    describe("read/write handles", function()
        it("can read and write to a file", function()
            write_file(file, "an example file")

            local handle = io.open(file, "r+")
            expect(handle:read(3)):eq("an ")

            handle:write("exciting file")
            expect(handle:seek("cur")):eq(16)

            handle:seek("set", 0)
            expect(handle:read("*a")):eq("an exciting file")
        end)
    end)
end)
