local t = require('spec.zync_test')

local function reload_zync()
    package.loaded['zync'] = nil
    package.loaded['zync.api'] = nil
    package.loaded['zync.api.commands.commands'] = nil
    package.loaded['zync.api.commands.Command'] = nil
    package.loaded['zync.api.configurator'] = nil
    package.loaded['zync.api.configurator.sandbox'] = nil
    package.loaded['argparse'] = nil
    return require('zync')
end

local function stub_manager_handlers()
    local manager = require('zync.api.manager')
    local calls = {}
    local stubs = {}
    local handlers = {
        init = 'init',
        conf = 'configure',
        build = 'build',
        flash = 'flash',
        run = 'run',
        clean = 'clean',
        boards = 'boards',
        runners = 'runners',
    }

    for command, handler in pairs(handlers) do
        calls[command] = {}
        stubs[handler] = t.stub(manager, handler, function(config)
            table.insert(calls[command], config)
        end)
    end

    return stubs, calls
end

local function stub_configurator_config(config)
    local configurator = require('zync.api.configurator')
    return t.stub(configurator, 'init_conf', function()
        return config
    end)
end

local function stub_lfs_symlinkattributes(value)
    return t.stub(t.lfs, 'symlinkattributes', function(_)
        return value
    end)
end

local function stub_lfs_attributes(value)
    return t.stub(t.lfs, 'attributes', function(_)
        return value
    end)
end

local function revert_stubs(stubs)
    for _, stub in pairs(stubs) do
        stub:revert()
    end
end

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('zync top-level API', function()
    local print_stub

    before_each(function()
        t.helpers.setup()
        print_stub = t.stub(_G, 'print')
    end)

    after_each(function()
        print_stub:revert()
        t.helpers.teardown()
    end)

    it('loads zync without crashing', function()
        local z = reload_zync()
        t.assert.is_truthy(z)
        t.assert.is_table(z.api)
    end)

    it('delegates zync.run to api.run', function()
        local z = reload_zync()
        local run_stub = t.stub(z.api, 'run', function() end)

        z.run()

        t.assert.stub(run_stub).was.called()
        run_stub:revert()
    end)

    it('prints version and exits when requested', function()
        local z = reload_zync()
        local og_args = z.api.args
        z.api.args = { version = true }
        t.exit_handler = function()
            error('exit')
        end

        local ok, err = pcall(function()
            z.api.version()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(err:match('exit'))
        t.assert.stub(print_stub).was_called_with(_G.zync.CMD .. ' v' .. _G.zync.VERSION)

        z.api.args = og_args
    end)

    it('runs default build command when no command is selected', function()
        local stubs, calls = stub_manager_handlers()
        local z = reload_zync()

        z.api.args = {}
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.equal(1, #calls.build)
        revert_stubs(stubs)
    end)

    it('dispatches command and merges cli args into config', function()
        t.helpers.write_file('zconf.lua', "return { board = 'conf_board', runner = 'conf_runner' }")

        local stubs, calls = stub_manager_handlers()
        local z = reload_zync()

        z.api.args = { build = true, board = 'cli_board' }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.equal(1, #calls.build)
        t.assert.equal('cli_board', calls.build[1].board)
        t.assert.equal('conf_runner', calls.build[1].runner)
        t.assert.equal(t.tmp, calls.build[1].cwd)

        revert_stubs(stubs)
    end)

    it('parses global profile option and applies it to config', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
                sim = { board = 'sim_board', runner = 'sim_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        local og_arg = _G.arg
        _G.arg = { '--profile', 'sim' }

        local z = reload_zync()

        t.assert.equals('sim_board', z.api.config.board)
        t.assert.equals('sim_runner', z.api.config.runner)
        t.assert.equals('sim', z.api.config.profile)

        _G.arg = og_arg
    end)

    it('ignores --profile when --all is provided', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
                sim = { board = 'sim_board', runner = 'sim_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        local og_arg = _G.arg
        _G.arg = { '--all', '--profile', 'sim' }

        local z = reload_zync()

        t.assert.equals('prod', z.api.config.profile)

        _G.arg = og_arg
    end)

    it('runs profile-aware commands for all profiles when --all is set', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                sim = { board = 'sim_board', runner = 'sim_runner' },
                prod = { board = 'prod_board', runner = 'prod_runner' },
            },
            default_profile = 'sim',
        }
        ]]
        )

        local stubs, calls = stub_manager_handlers()
        local z = reload_zync()

        z.api.args = { build = true, all = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.equal(2, #calls.build)
        t.assert.equals('sim', calls.build[1].profile)
        t.assert.equals('prod', calls.build[2].profile)

        revert_stubs(stubs)
    end)

    it('returns nil for helper profile resolution when config missing', function()
        local z = reload_zync()

        local ensure = get_upvalue(z.api.run, 'ensure_compile_commands_link')
        t.assert.equals('function', type(ensure))

        local resolve_default = get_upvalue(ensure, 'resolve_default_profile')
        t.assert.equals('function', type(resolve_default))
        t.assert.is_nil(resolve_default(nil))
        t.assert.is_nil(resolve_default({}))

        local resolve_profiles = get_upvalue(z.api.run, 'resolve_profiles')
        t.assert.equals('function', type(resolve_profiles))
        t.assert.is_nil(resolve_profiles(nil))
        t.assert.is_nil(resolve_profiles({}))
    end)

    it('skips compile_commands link when config is invalid', function()
        local stubs = stub_manager_handlers()
        local z = reload_zync()
        z.api.config = {
            invalid = true,
            profiles = { prod = {} },
            project_dir = t.tmp,
        }

        local called = false
        local link_stub = t.stub(t.lfs, 'symlinkattributes', function(_)
            called = true
            return nil
        end)

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.is_false(called)

        link_stub:revert()
        revert_stubs(stubs)
    end)

    it('uses ln -sf fallback when lfs.link is unavailable', function()
        local stubs = stub_manager_handlers()
        local z = reload_zync()
        z.api.config = {
            profiles = { prod = {} },
            default_profile = 'prod',
            project_dir = t.tmp,
        }

        local exec_calls = {}
        local exec_stub = t.stub(_G.os, 'execute', function(cmd)
            table.insert(exec_calls, cmd)
            return true
        end)

        local link_stub = t.stub(t.lfs, 'symlinkattributes', function(_)
            return nil
        end)

        local og_link = t.lfs.link
        t.lfs.link = nil

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local target = t.tmp .. '/build/prod/compile_commands.json'
        local link_path = t.tmp .. '/build/compile_commands.json'
        local expected = 'ln -sf ' .. target .. ' ' .. link_path
        local has_ln = false
        for _, call in ipairs(exec_calls) do
            if call == expected then
                has_ln = true
            end
        end
        t.assert.is_true(has_ln)

        t.lfs.link = og_link
        link_stub:revert()
        exec_stub:revert()
        revert_stubs(stubs)
    end)

    it('skips compile_commands link when default profile cannot be resolved', function()
        local stubs = stub_manager_handlers()
        local z = reload_zync()
        z.api.config = {
            profiles = {},
            project_dir = t.tmp,
        }

        local exec_calls = {}
        local exec_stub = t.stub(_G.os, 'execute', function(cmd)
            table.insert(exec_calls, cmd)
            return true
        end)

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.equals(0, #exec_calls)

        exec_stub:revert()
        revert_stubs(stubs)
    end)

    it('runs --all profiles in alphabetical order when profile_order missing', function()
        local manager = require('zync.api.manager')
        local build_calls = {}
        local build_stub = t.stub(manager, 'build', function(config)
            table.insert(build_calls, config.profile)
            return 0
        end)

        local z = reload_zync()
        z.api.config = {
            profiles = { zeta = {}, alpha = {} },
            project_dir = t.tmp,
        }

        local configurator = require('zync.api.configurator')
        local init_stub = t.stub(configurator, 'init_conf', function(_, profile)
            return { profile = profile, profiles = z.api.config.profiles }
        end)

        local link_stub = t.stub(t.lfs, 'symlinkattributes', function(_)
            return { mode = 'link' }
        end)

        z.api.args = { build = true, all = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.equals('alpha', build_calls[1])
        t.assert.equals('zeta', build_calls[2])

        link_stub:revert()
        init_stub:revert()
        build_stub:revert()
    end)

    it('exits early when a --all profile run fails', function()
        local manager = require('zync.api.manager')
        local build_calls = {}
        local build_stub = t.stub(manager, 'build', function(config)
            table.insert(build_calls, config.profile)
            return 2
        end)

        local z = reload_zync()
        z.api.config = {
            profiles = { sim = {}, prod = {} },
            profile_order = { 'sim', 'prod' },
            project_dir = t.tmp,
        }

        local configurator = require('zync.api.configurator')
        local init_stub = t.stub(configurator, 'init_conf', function(_, profile)
            return { profile = profile, profiles = z.api.config.profiles }
        end)

        local link_stub = t.stub(t.lfs, 'symlinkattributes', function(_)
            return { mode = 'link' }
        end)

        z.api.args = { build = true, all = true }
        t.exit_handler = function(code)
            error(code)
        end

        local ok, err = pcall(function()
            z.api.run()
        end)

        t.assert.is_false(ok)
        t.assert.equals('2', tostring(err))
        t.assert.equals(1, #build_calls)
        t.assert.equals('sim', build_calls[1])

        link_stub:revert()
        init_stub:revert()
        build_stub:revert()
    end)

    it('logs active profile after command log when selected', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
                sim = { board = 'sim_board', runner = 'sim_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        local og_arg = _G.arg
        _G.arg = { '--profile', 'sim' }
        _G.ZYNC_LOGS = true

        local stubs = stub_manager_handlers()
        local z = reload_zync()

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local calls = print_stub.calls
        local function find_call(msg)
            for i, call in ipairs(calls) do
                if call.vals and call.vals[1] == msg then
                    return i
                end
            end
        end

        local cmd_idx = find_call("zync: Running command 'build'")
        if not cmd_idx then
            cmd_idx = find_call("zync: No command specified, defaulting to 'build'")
        end
        local prof_idx = find_call("zync: Using profile 'sim'")
        t.assert.is_truthy(prof_idx)
        if cmd_idx then
            t.assert.is_true(cmd_idx < prof_idx)
        end

        revert_stubs(stubs)
        _G.arg = og_arg
    end)

    it('logs when profiles are not configured after command log', function()
        t.helpers.write_file('zconf.lua', "return { board = 'conf_board', runner = 'conf_runner' }")

        local og_arg = _G.arg
        _G.arg = {}
        _G.ZYNC_LOGS = true

        local stubs = stub_manager_handlers()
        local z = reload_zync()

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local calls = print_stub.calls
        local function find_call(msg)
            for i, call in ipairs(calls) do
                if call.vals and call.vals[1] == msg then
                    return i
                end
            end
        end

        local cmd_idx = find_call("zync: Running command 'build'")
        if not cmd_idx then
            cmd_idx = find_call("zync: No command specified, defaulting to 'build'")
        end
        local prof_idx = find_call('zync: Profiles not configured - running in profile-less mode')
        t.assert.is_truthy(prof_idx)
        if cmd_idx then
            t.assert.is_true(cmd_idx < prof_idx)
        end

        revert_stubs(stubs)
        _G.arg = og_arg
    end)

    it('does not log profile status for init command', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        local og_arg = _G.arg
        _G.arg = { '--profile', 'prod' }
        _G.ZYNC_LOGS = true

        local stubs = stub_manager_handlers()
        local z = reload_zync()

        z.api.args = { init = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local calls = print_stub.calls
        local function has_call(msg)
            for _, call in ipairs(calls) do
                if call.vals and call.vals[1] == msg then
                    return true
                end
            end
            return false
        end

        t.assert.is_false(has_call("zync: Using profile 'prod'"))
        t.assert.is_false(has_call('zync: Profiles not configured - running in profile-less mode'))

        revert_stubs(stubs)
        _G.arg = og_arg
    end)

    it('creates compile_commands.json symlink for default profile', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
                sim = { board = 'sim_board', runner = 'sim_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        os.execute('mkdir -p build/prod')
        t.helpers.write_file('build/prod/compile_commands.json', '[]')

        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        local z = reload_zync()

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local link_attr = t.lfs.symlinkattributes('build/compile_commands.json')
        t.assert.is_truthy(link_attr)
        t.assert.equal('link', link_attr.mode)
        t.assert.equal('[]', t.helpers.read_file('build/compile_commands.json'))

        build_stub:revert()
    end)

    it('keeps existing compile_commands.json symlink intact', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
                sim = { board = 'sim_board', runner = 'sim_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        os.execute('mkdir -p build/prod')
        t.helpers.write_file('build/prod/compile_commands.json', '[1]')
        os.execute('ln -s prod/compile_commands.json build/compile_commands.json')

        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        local z = reload_zync()

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local link_attr = t.lfs.symlinkattributes('build/compile_commands.json')
        t.assert.is_truthy(link_attr)
        t.assert.equal('link', link_attr.mode)
        t.assert.equal('[1]', t.helpers.read_file('build/compile_commands.json'))

        build_stub:revert()
    end)

    it('creates compile_commands.json link for first declared profile when default_profile missing', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                sim = { board = 'sim_board', runner = 'qemu' },
                prod = { board = 'prod_board', runner = 'openocd' },
            },
        }
        ]]
        )

        os.execute('mkdir -p build/sim build/prod')
        t.helpers.write_file('build/sim/compile_commands.json', '[sim]')
        t.helpers.write_file('build/prod/compile_commands.json', '[prod]')

        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        local z = reload_zync()

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local link_attr = t.lfs.symlinkattributes('build/compile_commands.json')
        t.assert.is_truthy(link_attr)
        t.assert.equal('link', link_attr.mode)
        t.assert.equal('[sim]', t.helpers.read_file('build/compile_commands.json'))

        build_stub:revert()
    end)

    it('replaces existing compile_commands.json file with symlink', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        os.execute('mkdir -p build/prod')
        t.helpers.write_file('build/prod/compile_commands.json', '[prod]')
        t.helpers.write_file('build/compile_commands.json', '[stale]')

        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        local z = reload_zync()

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local link_attr = t.lfs.symlinkattributes('build/compile_commands.json')
        t.assert.is_truthy(link_attr)
        t.assert.equal('link', link_attr.mode)
        t.assert.equal('[prod]', t.helpers.read_file('build/compile_commands.json'))

        build_stub:revert()
    end)

    it('does not create compile_commands.json symlink when profiles are not configured', function()
        t.helpers.write_file('zconf.lua', "return { board = 'conf_board', runner = 'conf_runner' }")

        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        local z = reload_zync()

        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.is_nil(t.lfs.symlinkattributes('build/compile_commands.json'))

        build_stub:revert()
    end)

    it('does not create compile_commands.json symlink for excluded commands', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        local manager = require('zync.api.manager')
        local stubs = {
            init = t.stub(manager, 'init', function(_) end),
            boards = t.stub(manager, 'boards', function(_) end),
            runners = t.stub(manager, 'runners', function(_) end),
            profiles = t.stub(manager, 'profiles', function(_) end),
        }

        local cases = {
            { name = 'init', args = { init = true } },
            { name = 'boards', args = { boards = true } },
            { name = 'runners', args = { runners = true } },
            { name = 'profiles', args = { profiles = true } },
        }

        for _, case in ipairs(cases) do
            local z = reload_zync()
            z.api.args = case.args
            t.exit_handler = function()
                error('exit')
            end
            pcall(function()
                z.api.run()
            end)

            t.assert.is_nil(t.lfs.symlinkattributes('build/compile_commands.json'))
        end

        for _, stub in pairs(stubs) do
            stub:revert()
        end
    end)

    it('skips compile_commands link when config is invalid', function()
        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        t.helpers.write_file('zconf.lua', "return { board = os.getenv('ZEPHYR_BASE') }")

        local ok, err = pcall(function()
            reload_zync()
        end)

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

        t.assert.is_nil(t.lfs.symlinkattributes('build/compile_commands.json'))

        build_stub:revert()
    end)

    it('skips compile_commands link when no profiles are configured', function()
        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        t.helpers.write_file('zconf.lua', "return { board = 'conf_board', runner = 'conf_runner' }")

        local z = reload_zync()
        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.is_nil(t.lfs.symlinkattributes('build/compile_commands.json'))

        build_stub:revert()
    end)

    it('falls back to alphabetical profile when no profile_order available', function()
        os.execute('rm -rf build')
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                zeta = { board = 'zeta_board', runner = 'openocd' },
                alpha = { board = 'alpha_board', runner = 'openocd' },
            },
        }
        ]]
        )
        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        local link_calls = {}
        local link_stub = t.stub(t.lfs, 'link', function(target, link_path, _)
            table.insert(link_calls, { target = target, link_path = link_path })
            return true
        end)

        local z = reload_zync()
        z.api.config.profile_order = {}
        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.is_true(#link_calls > 0)
        t.assert.is_truthy(link_calls[1].target:match('build/alpha/compile_commands.json$'))

        build_stub:revert()
        link_stub:revert()
    end)

    it('falls back to available profile when profile_order is invalid', function()
        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                sim = { board = 'conf_board', runner = 'conf_runner' },
            },
        }
        ]]
        )
        os.execute('mkdir -p build/sim')
        t.helpers.write_file('build/sim/compile_commands.json', '[sim]')

        local z = reload_zync()
        z.api.config.profile_order = { 'missing' }
        z.api.config.default_profile = ''
        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.equal('[sim]', t.helpers.read_file('build/compile_commands.json'))

        build_stub:revert()
    end)

    it('returns early when compile_commands path is a non-link non-file', function()
        os.execute('rm -rf build')
        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                sim = { board = 'conf_board', runner = 'conf_runner' },
            },
            default_profile = 'sim',
        }
        ]]
        )
        local link_stub = stub_lfs_symlinkattributes({ mode = 'directory' })
        local link_call_stub = t.stub(t.lfs, 'link', function(_, _, _)
            return true
        end)
        local os_execute_stub = t.stub(_G.os, 'execute')

        local z = reload_zync()
        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.stub(link_call_stub).was_not.called()

        link_stub:revert()
        link_call_stub:revert()
        os_execute_stub:revert()
        build_stub:revert()
    end)

    it('uses lfs.attributes branch when symlinkattributes is unavailable', function()
        os.execute('rm -rf build')
        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                sim = { board = 'conf_board', runner = 'conf_runner' },
            },
            default_profile = 'sim',
        }
        ]]
        )
        local attr_stub = stub_lfs_attributes({ mode = 'file' })
        local og_symlinkattributes = t.lfs.symlinkattributes
        t.lfs.symlinkattributes = nil
        local link_call_stub = t.stub(t.lfs, 'link', function(_, _, _)
            return true
        end)

        local z = reload_zync()
        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.stub(link_call_stub).was_not.called()

        build_stub:revert()
        if attr_stub then
            attr_stub:revert()
        end
        t.lfs.symlinkattributes = og_symlinkattributes
        link_call_stub:revert()
    end)

    it('falls back to ln -sf when lfs.link fails', function()
        os.execute('rm -rf build')
        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                sim = { board = 'conf_board', runner = 'conf_runner' },
            },
            default_profile = 'sim',
        }
        ]]
        )

        os.execute('mkdir -p build/sim')
        t.helpers.write_file('build/sim/compile_commands.json', '[sim]')

        local link_stub = t.stub(t.lfs, 'link', function(_, _, _)
            return false
        end)
        local os_execute_stub = t.stub(_G.os, 'execute')

        local z = reload_zync()
        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        local found_ln = false
        for _, call in ipairs(os_execute_stub.calls) do
            if
                call.vals
                and type(call.vals[1]) == 'string'
                and call.vals[1]:match('^ln %-sf ')
                and call.vals[1]:match('compile_commands%.json')
            then
                found_ln = true
                break
            end
        end
        t.assert.is_true(found_ln)

        os_execute_stub:revert()
        link_stub:revert()
        build_stub:revert()
    end)

    it('uses cwd when project_dir is missing for compile_commands link creation', function()
        os.execute('rm -rf build')
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                sim = { board = 'conf_board', runner = 'conf_runner' },
            },
            default_profile = 'sim',
        }
        ]]
        )

        os.execute('mkdir -p build/sim')
        t.helpers.write_file('build/sim/compile_commands.json', '[sim]')

        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        local link_calls = {}
        local link_stub = t.stub(t.lfs, 'link', function(target, link_path, _)
            table.insert(link_calls, { target = target, link_path = link_path })
            return true
        end)

        local z = reload_zync()
        z.api.config.project_dir = ''
        z.api.args = { build = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.is_true(#link_calls > 0)
        t.assert.is_truthy(link_calls[1].target:match(t.tmp .. '/build/sim/compile_commands.json$'))

        build_stub:revert()
        link_stub:revert()
    end)

    it('removes build dir for clean command in profile-less projects', function()
        t.helpers.write_file('zconf.lua', "return { board = 'conf_board', runner = 'conf_runner' }")
        os.execute('mkdir -p build/subdir')
        t.helpers.write_file('build/subdir/marker.txt', 'keep')

        local z = reload_zync()
        z.api.args = { clean = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.helpers.dir_exists('build', false)
    end)

    it('removes build dir for clean command in profile projects', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
                sim = { board = 'sim_board', runner = 'sim_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        os.execute('mkdir -p build/prod build/sim')
        t.helpers.write_file('build/prod/marker.txt', 'prod')
        t.helpers.write_file('build/sim/marker.txt', 'sim')

        local z = reload_zync()
        z.api.args = { clean = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.helpers.dir_exists('build', false)
    end)

    it('keeps other profile build dirs intact when pre-clean is requested', function()
        t.helpers.write_file(
            'zconf.lua',
            [[
        return {
            profiles = {
                prod = { board = 'conf_board', runner = 'conf_runner' },
                sim = { board = 'sim_board', runner = 'sim_runner' },
            },
            default_profile = 'prod',
        }
        ]]
        )

        os.execute('mkdir -p build/prod build/sim')
        t.helpers.write_file('build/prod/marker.txt', 'prod')
        t.helpers.write_file('build/sim/marker.txt', 'sim')

        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_) end)
        local z = reload_zync()

        z.api.args = { build = true, pre_clean = true }
        t.exit_handler = function()
            error('exit')
        end
        pcall(function()
            z.api.run()
        end)

        t.assert.is_nil(t.lfs.attributes('build/prod/marker.txt'))
        t.assert.is_truthy(t.lfs.attributes('build/sim/marker.txt'))

        build_stub:revert()
    end)

    it('dispatches all commands with parsed args', function()
        t.helpers.write_file('zconf.lua', "return { board = 'conf_board', runner = 'conf_runner' }")
        local stubs, calls = stub_manager_handlers()

        local cases = {
            { name = 'init', args = { init = true } },
            { name = 'conf', args = { conf = true, board = 'cli_board' } },
            { name = 'clean', args = { clean = true } },
            { name = 'build', args = { build = true, board = 'cli_board', runner = 'cli_runner' } },
            {
                name = 'flash',
                args = { flash = true, board = 'cli_board', runner = 'cli_runner', image = 2, list = true },
            },
            { name = 'run', args = { run = true, image = 1 } },
            { name = 'boards', args = { boards = true } },
            { name = 'runners', args = { runners = true } },
        }

        for _, case in ipairs(cases) do
            local z = reload_zync()
            z.api.args = case.args
            t.exit_handler = function()
                error('exit')
            end
            pcall(function()
                z.api.run()
            end)

            t.assert.equal(1, #calls[case.name])
            if case.args.board then
                t.assert.equal(case.args.board, calls[case.name][1].board)
            elseif case.name ~= 'boards' and case.name ~= 'runners' then
                t.assert.equal('conf_board', calls[case.name][1].board)
            end

            if case.args.runner then
                t.assert.equal(case.args.runner, calls[case.name][1].runner)
            elseif case.name ~= 'boards' and case.name ~= 'runners' then
                t.assert.equal('conf_runner', calls[case.name][1].runner)
            end

            if case.args.image then
                t.assert.equal(tonumber(case.args.image), calls[case.name][1].image)
            end

            if case.args.list then
                t.assert.is_true(calls[case.name][1].list)
            end

            calls[case.name] = {}
        end

        revert_stubs(stubs)
    end)

    it('exits with handler exit code', function()
        local manager = require('zync.api.manager')
        local build_stub = t.stub(manager, 'build', function(_)
            return 7
        end)
        local z = reload_zync()

        z.api.args = { build = true }
        t.exit_handler = function(code)
            error('exit:' .. tostring(code))
        end

        local ok, err = pcall(function()
            z.api.run()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(err:match('exit:7'))

        build_stub:revert()
    end)

    it('runs clean before selected command when requested', function()
        local manager = require('zync.api.manager')
        local order = {}
        local clean_stub = t.stub(manager, 'clean', function(_)
            table.insert(order, 'clean')
        end)
        local build_stub = t.stub(manager, 'build', function(_)
            table.insert(order, 'build')
        end)
        local z = reload_zync()

        z.api.args = { build = true, pre_clean = true }
        t.exit_handler = function()
            error('exit')
        end

        pcall(function()
            z.api.run()
        end)

        t.helpers.assert_tables_equal(order, { 'clean', 'build' })

        clean_stub:revert()
        build_stub:revert()
    end)

    it('exits when pre-clean fails', function()
        local manager = require('zync.api.manager')
        local clean_stub = t.stub(manager, 'clean', function(_)
            return 5
        end)
        local build_stub = t.stub(manager, 'build', function(_)
            return 0
        end)
        local z = reload_zync()

        z.api.args = { build = true, pre_clean = true }
        t.exit_handler = function(code)
            error('exit:' .. tostring(code))
        end

        local ok, err = pcall(function()
            z.api.run()
        end)

        t.assert.is_false(ok)
        t.assert.is_truthy(err:match('exit:5'))

        clean_stub:revert()
        build_stub:revert()
    end)
end)
