local t = require('spec.zync_test')
local fixture = require('spec.fixtures.configurator_fix')

local function get_upvalue(fn, name)
    local i = 1
    while true do
        local up_name, up_val = debug.getupvalue(fn, i)
        if not up_name then
            return nil
        end
        if up_name == name then
            return up_val
        end
        i = i + 1
    end
end

describe('Configurator lib module', function()
    local getenv_stub
    local og_getenv

    before_each(function()
        t.helpers.setup()
        og_getenv = os.getenv
        getenv_stub = t.stub(_G.os, 'getenv', function(key)
            if key == 'ZEPHYR_BASE' or key == 'ZEPHYR_WORKSPACE' then
                return ''
            end
            return og_getenv(key)
        end)
    end)

    after_each(function()
        if getenv_stub then
            getenv_stub:revert()
        end
        t.helpers.teardown()
    end)

    it('should init configurator workspace', function()
        for _, test_case in ipairs(fixture) do
            for _, dir in ipairs(test_case.dirs) do
                os.execute('mkdir -p ' .. dir)
            end

            for _, file in ipairs(test_case.files) do
                t.helpers.write_file(file.path, file.content)
            end

            t.lfs.chdir(test_case.dirs[test_case.dst])

            local res = require('zync.api.configurator').init_conf()
            t.helpers.assert_tables_equal(res, fixture.build_exp(test_case, t.tmp))

            t.lfs.chdir(t.tmp)
            os.execute('rm -rf ' .. test_case.dirs[1])
            package.loaded['zconf'] = nil
        end
    end)

    it('selects default_profile when profiles are defined', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'prod_board', runner = 'openocd' },
                sim = { board = 'native_sim', runner = 'qemu' },
            },
            default_profile = 'sim',
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.equals('sim', res.profile)
        t.assert.equals('native_sim', res.board)
        t.assert.equals('qemu', res.runner)
    end)

    it('defaults to first profile in config order when default_profile is absent', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'prod_board', runner = 'openocd' },
                sim = { board = 'native_sim', runner = 'qemu' },
            },
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.equals('prod', res.profile)
        t.assert.equals('prod_board', res.board)
        t.assert.equals('openocd', res.runner)
    end)

    it('errors on unknown default_profile and lists available profiles', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'prod_board', runner = 'openocd' },
                sim = { board = 'native_sim', runner = 'qemu' },
            },
            default_profile = 'missing',
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local ok, err = pcall(function()
            require('zync.api.configurator').init_conf()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(err:match("unknown profile 'missing'"))
        t.assert.is_truthy(err:match('Available profiles: prod, sim'))
    end)

    it('preserves profile order from source with mixed key styles', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'prod_board', runner = 'openocd' },
                -- comment between entries
                ['sim'] = { board = 'native_sim', runner = 'qemu' },
                ["test"] = { board = 'test_board', runner = 'dummy' },
            },
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.profile_order, { 'prod', 'sim', 'test' })
        t.assert.equals('prod', res.profile)
        t.assert.equals('prod_board', res.board)
        t.assert.equals('openocd', res.runner)
    end)

    it('errors when profiles is not a table', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = 'bad',
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local ok, err = pcall(function()
            require('zync.api.configurator').init_conf()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(tostring(err):match("field 'profiles' has to be table"))
    end)

    it('errors when sandboxed config accesses forbidden globals', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(app_dir .. '/zconf.lua', "return { board = os.getenv('ZEPHYR_BASE') }")

        t.lfs.chdir(app_dir)
        local ok, err = pcall(function()
            require('zync.api.configurator').init_conf()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(tostring(err):match("access to global 'os' is not allowed"))
    end)

    it('marks config invalid when zconf returns nil', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(app_dir .. '/zconf.lua', "return nil")

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.is_true(res.invalid)
    end)

    it('errors when profiles table is empty', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = {},
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local ok, err = pcall(function()
            require('zync.api.configurator').init_conf()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(err:match('no profiles defined under zconf.lua profiles'))
    end)

    it('errors when selected profile entry is not a table', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = {
                prod = 'bad',
            },
            default_profile = 'prod',
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local ok, err = pcall(function()
            require('zync.api.configurator').init_conf()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(err:match("profile 'prod' must be a table"))
    end)

    it('resolves build path and profile when invoked from build/<profile>', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir .. '/build/sim')
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = {
                sim = { board = 'native_sim', runner = 'qemu' },
            },
            default_profile = 'sim',
        }
        ]]
        )

        t.lfs.chdir(app_dir .. '/build/sim')
        local res = require('zync.api.configurator').init_conf()

        t.assert.equals(app_dir, res.project_dir)
        t.assert.equals('build/sim', res.build_dir)
        t.assert.equals('sim', res.profile)
    end)

    it('parses profile order with block comments and escaped strings', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        return {
            profiles = {
                --[[ block comment before ]]
                ["sim\\\"mode"] = { board = 'native_sim', runner = 'qemu' },
                -- inline comment
                prod = { board = 'prod_board', runner = 'openocd' },
            },
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.profile_order, { 'sim\\"mode', 'prod' })
        t.assert.equals('sim\\"mode', res.profile)
    end)

    it('parses profiles after strings with backslash escapes', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        local note = "C:\\tmp\\path"
        return {
            profiles = {
                sim = { board = 'native_sim', runner = 'qemu' },
            },
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.profile_order, { 'sim' })
    end)

    it('parses profiles when comments appear around the assignment', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        return {
            -- leading comment
            profiles -- inline
            = -- another
            { -- block start
                sim = { board = 'native_sim', runner = 'qemu' },
            },
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.profile_order, { 'sim' })
    end)

    it('falls back to table iteration when profiles block missing', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(app_dir .. '/zconf.lua', "return { board = 'nucleo_f767zi' }")

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.is_nil(res.profile_order)
        t.assert.equals('nucleo_f767zi', res.board)
    end)

    it('errors when profile is specified but profiles not configured', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(app_dir .. '/zconf.lua', "return { board = 'nucleo_f767zi' }")

        t.lfs.chdir(app_dir)
        local ok, err = pcall(function()
            require('zync.api.configurator').init_conf(false, 'sim')
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(err:match("profile 'sim' not defined"))
    end)

    it('derives zephyr workspace from zephyr base', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(app_dir .. '/zconf.lua', "return { board = 'nucleo_f767zi' }")

        local og_getenv = os.getenv
        local getenv_stub = t.stub(_G.os, 'getenv', function(key)
            if key == 'ZEPHYR_BASE' then
                return t.tmp .. '/ws/zephyr'
            end
            if key == 'ZEPHYR_WORKSPACE' then
                return ''
            end
            return og_getenv(key)
        end)

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.equals(t.tmp .. '/ws/', res.zephyr_ws)
        t.assert.equals(t.tmp .. '/ws/zephyr', res.zephyr_base)

        getenv_stub:revert()
    end)

    it('derives zephyr base from zephyr workspace', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(app_dir .. '/zconf.lua', "return { board = 'nucleo_f767zi' }")

        local og_getenv = os.getenv
        local getenv_stub = t.stub(_G.os, 'getenv', function(key)
            if key == 'ZEPHYR_BASE' then
                return ''
            end
            if key == 'ZEPHYR_WORKSPACE' then
                return t.tmp .. '/ws'
            end
            return og_getenv(key)
        end)

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.equals(t.tmp .. '/ws', res.zephyr_ws)
        t.assert.equals(t.tmp .. '/ws/zephyr', res.zephyr_base)

        getenv_stub:revert()
    end)

    it('parses profiles after quoted strings before block', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        local note = "ignore { profiles = { fake = 1 } }"
        return {
            profiles = {
                sim = { board = 'native_sim', runner = 'qemu' },
                prod = { board = 'prod_board', runner = 'openocd' },
            },
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.profile_order, { 'sim', 'prod' })
        t.assert.equals('sim', res.profile)
    end)

    it('ignores non-quoted bracket keys when extracting profile order', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        return {
            profiles = {
                [1] = { board = 'native_sim', runner = 'qemu' },
                sim = { board = 'native_sim', runner = 'qemu' },
            },
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.is_true(t.helpers.array_contains(res.profile_order, 'sim'))
        t.assert.is_false(t.helpers.array_contains(res.profile_order, '1'))
    end)

    it('falls back to iterating profiles when block is not a literal', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        local function make_profiles()
            return {
                beta = { board = 'beta_board', runner = 'openocd' },
                alpha = { board = 'alpha_board', runner = 'openocd' },
            }
        end
        return {
            profiles = make_profiles(),
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.is_true(t.helpers.array_contains(res.profile_order, 'alpha'))
        t.assert.is_true(t.helpers.array_contains(res.profile_order, 'beta'))
    end)

    it('parses profiles with line and block comments', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        return {
            profiles = { -- inline comment
                --[[ block comment before ]]
                alpha = { board = 'alpha_board', runner = 'openocd' },
                -- comment between entries
                beta = { board = 'beta_board', runner = 'openocd' },
            },
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.profile_order, { 'alpha', 'beta' })
    end)

    it('falls back to table iteration when profiles block is in string', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        local fake = "profiles = { nope = 1 }"
        return {
            profiles = {
                beta = { board = 'beta_board', runner = 'openocd' },
                alpha = { board = 'alpha_board', runner = 'openocd' },
            },
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.is_truthy(res.profile_order)
        t.assert.is_true(t.helpers.array_contains(res.profile_order, 'alpha'))
        t.assert.is_true(t.helpers.array_contains(res.profile_order, 'beta'))
    end)

    it('interpolates nested tables and args recursively', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            board = 'native_sim',
            runner = 'qemu',
            args = {
                conf = {
                    '-DROOT=${project_dir}',
                    '-DBOARD=${board}',
                    { nested = { value = '${project_dir}/app' } },
                },
                build = '--build-dir ${build_dir}',
                run = { '${project_dir}/bin', '${board}' },
            },
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.args.conf, {
            '-DROOT=' .. app_dir,
            '-DBOARD=native_sim',
            { nested = { value = app_dir .. '/app' } },
        })
        t.assert.equals('--build-dir build', res.args.build)
        t.helpers.assert_tables_equal(res.args.run, { app_dir .. '/bin', 'native_sim' })

        package.loaded['zconf'] = nil
    end)

    it('interpolates profile-aware placeholders in profile args', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            board = 'global_board',
            runner = 'openocd',
            profiles = {
                sim = {
                    board = 'native_sim',
                    args = {
                        conf = { '-DBOARD=${board}' },
                        build = '--build-dir ${build_dir}',
                        run = { '${project_dir}/run', '${board}' },
                    },
                },
                prod = {
                    board = 'prod_board',
                    args = {
                        conf = { '-DBOARD=${board}' },
                        build = '--build-dir ${build_dir}',
                        run = { '${project_dir}/run', '${board}' },
                    },
                },
            },
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.profiles.sim.args.conf, { '-DBOARD=native_sim' })
        t.assert.equals('--build-dir build/sim', res.profiles.sim.args.build)
        t.helpers.assert_tables_equal(res.profiles.sim.args.run, { app_dir .. '/run', 'native_sim' })
        t.helpers.assert_tables_equal(res.profiles.prod.args.conf, { '-DBOARD=prod_board' })
        t.assert.equals('--build-dir build/prod', res.profiles.prod.args.build)
        t.helpers.assert_tables_equal(res.profiles.prod.args.run, { app_dir .. '/run', 'prod_board' })

        package.loaded['zconf'] = nil
    end)

    it('uses profile override values when interpolating build dir and zephyr paths', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            board = 'global_board',
            runner = 'openocd',
            zephyr_ws = '/global/ws',
            zephyr_base = '/global/ws/zephyr',
            profiles = {
                sim = {
                    build_dir = 'out/sim',
                    zephyr_ws = '/sim/ws',
                    zephyr_base = '/sim/ws/zephyr',
                    args = {
                        conf = { '-DWS=${zephyr_ws}', '-DZEPHYR=${zephyr_base}' },
                        build = '--build-dir ${build_dir}',
                    },
                },
            },
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.helpers.assert_tables_equal(res.profiles.sim.args.conf, { '-DWS=/sim/ws', '-DZEPHYR=/sim/ws/zephyr' })
        t.assert.equals('--build-dir out/sim', res.profiles.sim.args.build)

        package.loaded['zconf'] = nil
    end)

    it('uses profile build_dir override when interpolating args', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            profiles = {
                sim = {
                    build_dir = 'build/custom',
                    args = {
                        build = '--build-dir ${build_dir}',
                    },
                },
            },
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.equals('--build-dir build/custom', res.profiles.sim.args.build)

        package.loaded['zconf'] = nil
    end)

    it('leaves configs without placeholders unchanged', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [[
        return {
            board = 'plain_board',
            runner = 'plain_runner',
            args = {
                conf = {
                    '-DPLAIN=ON',
                    { nested = { key = 'value' } },
                },
                build = { '-j', '2' },
                run = { '--flag', 'ok' },
            },
        }
        ]]
        )

        t.lfs.chdir(app_dir)
        local res = require('zync.api.configurator').init_conf()

        t.assert.equals('plain_board', res.board)
        t.assert.equals('plain_runner', res.runner)
        t.helpers.assert_tables_equal(res.args.conf, { '-DPLAIN=ON', { nested = { key = 'value' } } })
        t.helpers.assert_tables_equal(res.args.build, { '-j', '2' })
        t.helpers.assert_tables_equal(res.args.run, { '--flag', 'ok' })

        package.loaded['zconf'] = nil
    end)

    it('covers configurator parser helper edge branches', function()
        local configurator = require('zync.api.configurator')
        local init_conf = configurator.init_conf
        local apply_profile = get_upvalue(init_conf, 'apply_profile')
        local profile_order_from_config = get_upvalue(apply_profile, 'profile_order_from_config')
        local extract_profile_order = get_upvalue(profile_order_from_config, 'extract_profile_order')
        local skip_ws_and_comments = get_upvalue(extract_profile_order, 'skip_ws_and_comments')
        local scan_string = get_upvalue(extract_profile_order, 'scan_string')

        local block_comment_src = "--[[ leading block ]]\nreturn { profiles = { sim = { board = 'x' } } }"
        t.helpers.assert_tables_equal(extract_profile_order(block_comment_src), { 'sim' })

        local profiles_with_block_comment = "return { profiles --[[c]] = { sim = { board = 'x' } } }"
        t.helpers.assert_tables_equal(extract_profile_order(profiles_with_block_comment), { 'sim' })

        local quoted_key_src = 'return { profiles = { "sim" = { board = "x" } } }'
        t.helpers.assert_tables_equal(extract_profile_order(quoted_key_src), { 'sim' })

        local unterminated_quote_src = 'local x = "unterminated\nreturn { profiles = { sim = { board = "x" } } }'
        t.helpers.assert_tables_equal(extract_profile_order(unterminated_quote_src), {})

        local str, next_i = scan_string('"unterminated', 1)
        t.assert.equals('unterminated', str)
        t.assert.equals(#'"unterminated' + 1, next_i)

        local end_pos = skip_ws_and_comments('   ', 1)
        t.assert.equals(4, end_pos)
    end)

    it('errors when apply_profile receives non-table profiles', function()
        local configurator = require('zync.api.configurator')
        local init_conf = configurator.init_conf
        local apply_profile = get_upvalue(init_conf, 'apply_profile')

        local ok, err = pcall(function()
            apply_profile({ profiles = 'bad' }, '')
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(tostring(err):match("field 'profiles' has to be table"))
    end)

    it('handles unterminated comment patterns in parser helpers', function()
        local configurator = require('zync.api.configurator')
        local init_conf = configurator.init_conf
        local apply_profile = get_upvalue(init_conf, 'apply_profile')
        local profile_order_from_config = get_upvalue(apply_profile, 'profile_order_from_config')
        local extract_profile_order = get_upvalue(profile_order_from_config, 'extract_profile_order')

        local src_line = 'return { profiles = { -- trailing comment'
        local src_block = 'return { profiles = { --[[ unterminated'

        t.helpers.assert_tables_equal(extract_profile_order(src_line), {})
        t.helpers.assert_tables_equal(extract_profile_order(src_block), {})
    end)

    it('errors when profiles is not a table in non-build context', function()
        local app_dir = t.tmp .. '/app'
        os.execute('mkdir -p ' .. app_dir)
        t.helpers.write_file(
            app_dir .. '/zconf.lua',
            [=[
        return {
            profiles = 'bad',
        }
        ]=]
        )

        t.lfs.chdir(app_dir)
        local ok, err = pcall(function()
            require('zync.api.configurator').init_conf()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(tostring(err):match("field 'profiles' has to be table"))
    end)
end)
