-- Copyright (c) 2026 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.

local lu = require('luaunit')

-- ==================== TestMctpObject ====================

TestMctpObject = {}

local saved_pkg_obj = {}

local function save_obj_packages(keys)
    for _, k in ipairs(keys) do
        saved_pkg_obj[k] = package.loaded[k]
    end
end

local function restore_obj_packages()
    for k, v in pairs(saved_pkg_obj) do
        if v == nil then
            package.loaded[k] = nil
        else
            package.loaded[k] = v
        end
    end
    saved_pkg_obj = {}
end

local function setup_mctp_object_mocks(opts)
    opts = opts or {}
    package.loaded['mctp.mctp_object'] = nil

    local keys = { 'mctp_lib', 'mc.logging', 'common_def' }
    save_obj_packages(keys)

    package.loaded['mctp_lib'] = {
        bdf_to_phy_addr = opts.bdf_to_phy_addr or function(bus, dev, func)
            return 42
        end,
        get_endpoint_and_transport = opts.get_endpoint_and_transport or function(...)
            return {}, {}
        end,
    }

    package.loaded['mc.logging'] = {
        debug = function() end,
        info = function() end,
        notice = function() end,
        error = function() end,
    }

    package.loaded['common_def'] = {
        PMC_3152_8I_SMART_RAID = 64,
        PMC_2100_8I_SMART_HBA = 65,
        PMC_8204_8I_SMART_RAID = 66,
        PMC_8254_8I_SMART_RAID = 67,
        PMC_8222_8I_SMART_RAID = 68,
    }

    return require('mctp.mctp_object')
end

function TestMctpObject:tearDown()
    restore_obj_packages()
    package.loaded['mctp.mctp_object'] = nil
end

-- Covers lines 31, 32, 34, 35: bdf_to_phy_addr call and log:notice with controller fields
function TestMctpObject:test_new_pmc_raid()
    local mctp_obj_module = setup_mctp_object_mocks()

    local controller = {
        Id = 1,
        DevBus = 10,
        DevDevice = 5,
        DevFunction = 3,
        TypeId = 64, -- PMC_3152_8I_SMART_RAID
    }

    local obj = mctp_obj_module.new('mock_bus', controller)
    lu.assertEquals(obj.phy_addr, 42)
    lu.assertEquals(obj.id, 1)
    lu.assertEquals(obj.bus, 'mock_bus')
    lu.assertEquals(obj.vendor_id, 0x9005)
    lu.assertEquals(controller.Phyaddr, 42)
end

function TestMctpObject:test_new_huawei_raid()
    local mctp_obj_module = setup_mctp_object_mocks()

    local controller = {
        Id = 2,
        DevBus = 20,
        DevDevice = 0,
        DevFunction = 1,
        TypeId = 96, -- not PMC
    }

    local obj = mctp_obj_module.new('mock_bus', controller)
    lu.assertEquals(obj.phy_addr, 42)
    lu.assertEquals(obj.vendor_id, 0x1E81)
end

function TestMctpObject:test_is_pmc_raid()
    local mctp_obj_module = setup_mctp_object_mocks()
    lu.assertTrue(mctp_obj_module.is_pmc_raid(64))
    lu.assertTrue(mctp_obj_module.is_pmc_raid(65))
    lu.assertFalse(mctp_obj_module.is_pmc_raid(96))
    lu.assertFalse(mctp_obj_module.is_pmc_raid(0))
end

-- Covers lines 51, 52, 53, 55: pcall get_endpoint_and_transport
function TestMctpObject:test_register_ctrl_mctp_transport_success()
    local transport_mock = { eid = 8 }
    local mctp_obj_module = setup_mctp_object_mocks({
        get_endpoint_and_transport = function(bus, name, phy_addr, msg_type)
            return { TargetEid = 8 }, transport_mock
        end,
    })

    local controller = {
        Id = 1,
        DevBus = 10,
        DevDevice = 5,
        DevFunction = 3,
        TypeId = 96,
    }

    local obj = mctp_obj_module.new('mock_bus', controller)
    obj:register_ctrl_mctp_transport()
    lu.assertEquals(obj.transport, transport_mock)
end

function TestMctpObject:test_register_ctrl_mctp_transport_fail()
    local mctp_obj_module = setup_mctp_object_mocks({
        get_endpoint_and_transport = function(...)
            error('transport error')
        end,
    })

    local controller = {
        Id = 1,
        DevBus = 10,
        DevDevice = 5,
        DevFunction = 3,
        TypeId = 96,
    }

    local obj = mctp_obj_module.new('mock_bus', controller)
    local ok = pcall(obj.register_ctrl_mctp_transport, obj)
    lu.assertFalse(ok)
end

-- ==================== TestMctpService ====================

TestMctpService = {}

local saved_pkg_svc = {}

local function save_svc_packages(keys)
    for _, k in ipairs(keys) do
        saved_pkg_svc[k] = package.loaded[k]
    end
end

local function restore_svc_packages()
    for k, v in pairs(saved_pkg_svc) do
        if v == nil then
            package.loaded[k] = nil
        else
            package.loaded[k] = v
        end
    end
    saved_pkg_svc = {}
end

local function setup_mctp_service_mocks(opts)
    opts = opts or {}
    package.loaded['mctp.mctp_service'] = nil

    local keys = {
        'mctp.mctp_object',
        'mc.logging',
        'mc.singleton',
        'sml.core',
        'sml',
        'mc.orm.tasks',
        'mc.class',
        'mctp_lib',
        'storage_bus',
    }
    save_svc_packages(keys)

    -- Mock mctp_object
    package.loaded['mctp.mctp_object'] = {
        new = function(bus, controller)
            return {
                id = controller.Id,
                eid = 0,
                phy_addr = 42,
                vendor_id = 0x1E81,
                register_ctrl_mctp_transport = function() end,
            }
        end,
        is_pmc_raid = function(type_id)
            return false
        end,
    }

    -- Mock mc.logging
    local log_calls = {}
    package.loaded['mc.logging'] = {
        debug = function() end,
        info = function() end,
        notice = function(_, fmt, ...)
            table.insert(log_calls, string.format(fmt, ...))
        end,
        error = function() end,
    }

    -- Mock mc.class
    package.loaded['mc.class'] = function()
        local cls = {}
        function cls.new(...)
            local instance = {}
            setmetatable(instance, { __index = cls })
            local ctor = rawget(cls, 'ctor')
            if ctor then
                ctor(instance, ...)
            end
            local init = rawget(cls, 'init')
            if init then
                init(instance, ...)
            end
            return instance
        end
        return cls
    end

    -- Mock mc.singleton
    local singleton_instance
    package.loaded['mc.singleton'] = function(cls)
        local wrapped = {}
        function wrapped.new(...)
            if not singleton_instance then
                singleton_instance = cls.new(...)
            end
            return singleton_instance
        end
        function wrapped.get_instance()
            return singleton_instance
        end
        function wrapped.destroy()
            singleton_instance = nil
        end
        return wrapped
    end

    -- Mock sml.core and sml
    local mock_mctp_infos = {
        set_bmc_mctp_info = function() end,
        add_mctp_info = function() end,
        del_mctp_info = function() end,
    }
    package.loaded['sml.core'] = { mctp_infos = mock_mctp_infos }
    package.loaded['sml'] = { mctp_infos = mock_mctp_infos }

    -- Mock mc.orm.tasks
    local task_mock = {}
    task_mock._stopped = false
    task_mock.loop = function(self, fn)
        task_mock._fn = fn
        return task_mock
    end
    task_mock.set_timeout_ms = function(self, ms)
        return task_mock
    end
    task_mock.stop = function(self)
        task_mock._stopped = true
    end
    package.loaded['mc.orm.tasks'] = {
        get_instance = function()
            return {
                new_task = function(name)
                    return task_mock
                end,
            }
        end,
    }

    -- Mock mctp_lib
    package.loaded['mctp_lib'] = {
        get_mctp_pcie_binding = opts.get_mctp_pcie_binding or function(bus)
            return { BmcEid = 1, BmcPhyAddr = 2 }
        end,
        get_pcie_endpoint = function(...)
            return { TargetEid = 8 }
        end,
        MCTP_MESSAGE_TYPE_VDPCI = 0x7E,
    }

    -- Mock storage_bus
    package.loaded['storage_bus'] = {
        get_instance = function()
            return { bus = 'mock_bus' }
        end,
    }

    local mctp_service = require('mctp.mctp_service')
    return {
        module = mctp_service,
        task_mock = task_mock,
        log_calls = log_calls,
        mock_mctp_infos = mock_mctp_infos,
    }
end

function TestMctpService:tearDown()
    restore_svc_packages()
    saved_pkg_svc = {}
    package.loaded['mctp.mctp_service'] = nil
end

-- Covers line 36: task creation in init()
function TestMctpService:test_init_creates_task()
    local ctx = setup_mctp_service_mocks()
    local svc = ctx.module.new()

    svc:init()

    lu.assertNotNil(svc.mctp_start_task)
    lu.assertNotNil(ctx.task_mock._fn)
end

-- Covers lines 42, 43 (task:stop, mctp_start_task=nil) and 97, 99, 100 (prepare log:notice)
function TestMctpService:test_loop_with_mctp_state_true()
    local ctx = setup_mctp_service_mocks()
    local svc = ctx.module.new()

    svc.os_state = true
    svc:init()

    -- Call the loop function stored by init()
    local task = svc.mctp_start_task
    task._fn(task)

    -- prepare() should have set mctp_state = true
    lu.assertEquals(svc.mctp_state, true)
    -- task:stop() should have been called
    lu.assertTrue(ctx.task_mock._stopped)
    -- mctp_start_task should be nil
    lu.assertNil(svc.mctp_start_task)
    -- log:notice should have been called in prepare()
    lu.assertTrue(#ctx.log_calls > 0)
end

-- Covers lines 97, 99, 100: prepare() log:notice when os_state=true
function TestMctpService:test_prepare_success()
    local ctx = setup_mctp_service_mocks()
    local svc = ctx.module.new()

    svc.os_state = true
    svc:prepare()

    lu.assertEquals(svc.mctp_state, true)
    lu.assertTrue(#ctx.log_calls > 0)
    local found = false
    for _, msg in ipairs(ctx.log_calls) do
        if string.find(msg, 'mctp prepare finished') then
            found = true
            break
        end
    end
    lu.assertTrue(found)
end

function TestMctpService:test_prepare_os_state_false()
    local ctx = setup_mctp_service_mocks()
    local svc = ctx.module.new()

    svc.os_state = false
    svc:prepare()

    lu.assertEquals(svc.mctp_state, false)
end

function TestMctpService:test_prepare_no_binding()
    local ctx = setup_mctp_service_mocks({
        get_mctp_pcie_binding = function(bus)
            return nil
        end,
    })
    local svc = ctx.module.new()

    svc.os_state = true
    svc:prepare()

    lu.assertEquals(svc.mctp_state, false)
end