local t = require('spec.zync_test')
local Command = require('zync.api.commands.Command')
local parser_stub = require('spec.stubs.parser_stub')
local utils = require('zync.lib.utils')
local spy = require('luassert.spy')

local assert = t.assert

describe('Command class', function()
    local cmd_args

    before_each(function()
        -- common test fixtures for args
        cmd_args = {
            board = {
                flag = 'b',
                desc = 'App board',
                arg = 'board',
            },
            runner = {
                flag = 'r',
                desc = 'Board runner',
                arg = 'runner',
            },
            alt_runner = {
                flag = 'a',
                desc = 'Alternative runner',
                arg = 'alt_runner',
                skip_if = 'runner',
            },
            verbose = {
                flag = 'v',
                desc = 'Verbose output',
                -- no arg => option
            },
        }

        t.helpers.setup()
        parser_stub.load()
    end)

    after_each(function()
        parser_stub.revert()
        t.helpers.teardown()
    end)

    it('constructs a Command and wires parser + handler', function()
        local handler = spy.new(function(_) end)

        local cmd = Command:new(parser_stub, 'build', 'b', 'Build app', 'Build app using Ninja', handler)

        assert.is_table(cmd)
        assert.equals(Command, getmetatable(cmd))
        assert.equals(parser_stub, cmd.parser)
        assert.equals(handler, cmd.handler)

        -- argparse call chain: require_command(false):command(name):summary(summary):description(desc)
        assert.stub(parser_stub.require_command).was.called(1)
        assert.stub(parser_stub.require_command).was.called_with(parser_stub, false)

        assert.stub(parser_stub.command).was.called(1)
        assert.stub(parser_stub.command).was.called_with(parser_stub, 'build b')

        assert.stub(parser_stub.summary).was.called(1)
        assert.stub(parser_stub.summary).was.called_with(parser_stub, 'Build app')

        assert.stub(parser_stub.description).was.called(1)
        assert.stub(parser_stub.description).was.called_with(parser_stub, 'Build app using Ninja')
    end)

    it('does not set description when desc is nil/empty', function()
        local handler = spy.new(function(_) end)

        local cmd = Command:new(
            parser_stub,
            'config',
            'c', -- short
            'Configure app', -- summary
            nil, -- no desc
            handler
        )

        assert.is_table(cmd)
        assert.stub(parser_stub.description).was_not.called()
    end)

    it('args() stores args and registers options/flags with parser in sorted order', function()
        local handler = spy.new(function(_) end)
        local cmd = Command:new(parser_stub, 'build', 'b', 'Build app', 'Build app using Ninja', handler)

        cmd:args(cmd_args)

        -- verify args table stored
        assert.is_table(cmd.args)
        assert.is_true(t.helpers.table_contains(cmd.args, 'board'))
        assert.is_true(t.helpers.table_contains(cmd.args, 'runner'))
        assert.is_true(t.helpers.table_contains(cmd.args, 'alt_runner'))
        assert.is_true(t.helpers.table_contains(cmd.args, 'verbose'))

        -- verify sorted_args() ordering by arg name
        local sorted = cmd:sorted_args()
        local names = {}
        for i, sarg in ipairs(sorted) do
            names[i] = sarg.arg
        end

        -- alphabetical by key
        t.helpers.assert_tables_equal(names, { 'alt_runner', 'board', 'runner', 'verbose' })

        -- options: board, runner, alt_runner
        assert.stub(parser_stub.option).was.called(3)
        assert.stub(parser_stub.flag).was.called(1)

        -- create_arg() => "--<arg> -<flag>" for multi-char arg + single-char flag
        assert.stub(parser_stub.option).was.called_with(parser_stub, '--alt_runner -a', 'Alternative runner')
        assert.stub(parser_stub.option).was.called_with(parser_stub, '--board -b', 'App board')
        assert.stub(parser_stub.option).was.called_with(parser_stub, '--runner -r', 'Board runner')

        assert.stub(parser_stub.flag).was.called_with(parser_stub, '--verbose -v', 'Verbose output')

        -- every created option/flag should have args(1) called
        assert.stub(parser_stub.args).was.called(3)
        assert.stub(parser_stub.argname).was.called(3)
        assert.stub(parser_stub.convert).was.called(3)

        assert.stub(parser_stub.argname).was.called_with(parser_stub, '<alt_runner>')
        assert.stub(parser_stub.argname).was.called_with(parser_stub, '<board>')
        assert.stub(parser_stub.argname).was.called_with(parser_stub, '<runner>')
        assert.stub(parser_stub.convert).was.called_with(parser_stub, utils.tonumber_if_possible)
    end)

    it('parse() populates config from args and respects skip_if', function()
        local handler = spy.new(function(_) end)
        local cmd = Command:new(parser_stub, 'build', 'b', 'Build app', 'Build app using Ninja', handler)

        cmd:args(cmd_args)

        local cli_args = {
            board = 'nucleo_f767zi',
            runner = 'openocd',
            alt_runner = 'should_be_ignored',
            verbose = true,
        }

        local config = {}
        cmd:parse(cli_args, config)

        -- alt_runner skipped because skip_if = 'runner' and runner is present
        t.helpers.assert_tables_equal(config, {
            board = 'nucleo_f767zi',
            runner = 'openocd',
            verbose = true,
        })
    end)

    it('run() parses args and calls handler with config', function()
        local handler = spy.new(function(_) end)
        local cmd = Command:new(parser_stub, 'build', 'b', 'Build app', 'Build app using Ninja', handler)

        cmd:args(cmd_args)

        local cli_args = {
            board = 'nucleo_f767zi',
            runner = 'openocd',
        }

        local config = {}
        cmd:run(cli_args, config)

        -- handler was called exactly once with config
        assert.spy(handler).was.called(1)
        assert.spy(handler).was.called_with(config)

        t.helpers.assert_tables_equal(config, {
            board = 'nucleo_f767zi',
            runner = 'openocd',
        })
    end)
end)
