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

local lu = require('luaunit')

TestControllerFirmwareObject = {}

local saved_pkg = {}

function TestControllerFirmwareObject:setUp()
    saved_pkg = {}
end

function TestControllerFirmwareObject:tearDown()
    for k, v in pairs(saved_pkg) do
        if v == nil then
            package.loaded[k] = nil
        else
            package.loaded[k] = v
        end
    end
    saved_pkg = {}
    package.loaded['controller_firmware.controller_firmware_object'] = nil
end

-- ============================
-- Mock helper: set up controller_firmware_object module for isolated testing
-- ============================

local function setup_mocked_firmware_object(log_capture, find_returns)
    log_capture = log_capture or {}

    -- Save package.loaded state for cleanup
    saved_pkg['mc.logging'] = package.loaded['mc.logging']
    saved_pkg['skynet'] = package.loaded['skynet']
    saved_pkg['controller.controller_object'] = package.loaded['controller.controller_object']
    saved_pkg['mc.singleton'] = package.loaded['mc.singleton']
    saved_pkg['mc.class'] = package.loaded['mc.class']

    -- Unload the module
    package.loaded['controller_firmware.controller_firmware_object'] = nil

    -- Mock logging
    local mock_log = {}
    function mock_log:debug(fmt, ...)
        table.insert(log_capture, { level = 'debug', msg = string.format(fmt, ...) })
    end
    function mock_log:info(fmt, ...)
        table.insert(log_capture, { level = 'info', msg = string.format(fmt, ...) })
    end
    function mock_log:notice(fmt, ...)
        table.insert(log_capture, { level = 'notice', msg = string.format(fmt, ...) })
    end
    function mock_log:warn(fmt, ...)
        table.insert(log_capture, { level = 'warning', msg = string.format(fmt, ...) })
    end
    function mock_log:error(fmt, ...)
        table.insert(log_capture, { level = 'error', msg = string.format(fmt, ...) })
    end
    package.loaded['mc.logging'] = mock_log

    -- Mock skynet: fork executes synchronously, sleep does nothing
    local mock_skynet = {
        fork = function(cb, ...)
            if cb then
                cb(...)
            end
        end,
        sleep = function(_) end,
    }
    package.loaded['skynet'] = mock_skynet

    -- Mock controller.controller_object
    local find_call_count = 0
    local mock_c_controller = {
        collection = {
            find_by_position = function(_, position)
                find_call_count = find_call_count + 1
                if type(find_returns) == 'function' then
                    return find_returns(position, find_call_count)
                end
                return find_returns
            end,
        },
    }
    package.loaded['controller.controller_object'] = mock_c_controller

    -- Mock singleton
    local singleton_instance
    local mock_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
        return wrapped
    end
    package.loaded['mc.singleton'] = mock_singleton

    -- Mock class
    local mock_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
    package.loaded['mc.class'] = mock_class

    -- Now require the module
    local module = require('controller_firmware.controller_firmware_object')
    local instance = module.new()

    return {
        instance = instance,
        mock_log = mock_log,
        mock_c_controller = mock_c_controller,
        find_call_count = function()
            return find_call_count
        end,
    }
end

-- ============================
-- Tests: on_add_object - controller not found (covers log:warn line)
-- ============================

function TestControllerFirmwareObject:test_on_add_object_controller_not_found_warn()
    local log_capture = {}
    -- find_by_position always returns nil -> controller not found after retry
    local ctx = setup_mocked_firmware_object(log_capture, nil)

    local object = {
        ComponentID = 1,
        ComponentIDEx = 100,
    }
    local position = 'Controller_0_00010101'

    ctx.instance:on_add_object('ControllerFirmware', object, position)

    -- Verify log:warn was called with the expected message
    local found_warn = false
    for _, entry in ipairs(log_capture) do
        if entry.level == 'warning' and entry.msg:find('failed to find controller at position') then
            found_warn = true
            -- Verify position is in the message
            lu.assertTrue(entry.msg:find(position) ~= nil, 'warn message should contain position')
            break
        end
    end
    lu.assertTrue(found_warn, 'Should log warning when controller not found after retry')
end

function TestControllerFirmwareObject:test_on_add_object_wrong_class_ignored()
    local log_capture = {}
    local ctx = setup_mocked_firmware_object(log_capture, nil)

    -- Call with non-matching class name - should return early
    ctx.instance:on_add_object('NotControllerFirmware', {}, 'pos')

    -- Should only have the notice log from on_add_object line 86, no warn
    local found_warn = false
    for _, entry in ipairs(log_capture) do
        if entry.level == 'warning' then
            found_warn = true
            break
        end
    end
    lu.assertFalse(found_warn, 'Should not log warning for non-ControllerFirmware class')
end

-- ============================
-- Tests: on_add_object - controller found (success path)
-- ============================

function TestControllerFirmwareObject:test_on_add_object_controller_found_success()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 1,
        Id = 0,
    }
    -- find_by_position returns controller on first call
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    local object = {
        ComponentID = 1,
        ComponentIDEx = 100,
    }
    local position = 'Controller_0_00010101'

    ctx.instance:on_add_object('ControllerFirmware', object, position)

    -- Verify no warn log was emitted (controller was found)
    local found_warn = false
    for _, entry in ipairs(log_capture) do
        if entry.level == 'warning' then
            found_warn = true
            break
        end
    end
    lu.assertFalse(found_warn, 'Should not log warning when controller is found')

    -- Verify the firmware object was stored
    local firmware_info = ctx.instance.controller_firmware_objs[position]
    lu.assertNotNil(firmware_info, 'Firmware info should be stored')
    lu.assertEquals(firmware_info.firmware_obj.ComponentID, 1)
    lu.assertEquals(firmware_info.firmware_obj.ComponentIDEx, 100)
    lu.assertEquals(firmware_info.firmware_obj.Position, position)
end

function TestControllerFirmwareObject:test_on_add_object_controller_found_default_values()
    local log_capture = {}
    local mock_controller = {
        DeviceName = nil,
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    -- object without ComponentID/ComponentIDEx -> should default
    local object = {}
    local position = 'Controller_1_ABCDEF'

    ctx.instance:on_add_object('ControllerFirmware', object, position)

    local firmware_info = ctx.instance.controller_firmware_objs[position]
    lu.assertNotNil(firmware_info, 'Firmware info should be stored')
    lu.assertEquals(firmware_info.firmware_obj.ComponentID, 255)
    lu.assertEquals(firmware_info.firmware_obj.ComponentIDEx, 4294967295)
end

-- ============================
-- Tests: on_delete_object
-- ============================

function TestControllerFirmwareObject:test_on_delete_object_clears_record()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 1,
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    -- First add an object
    local position = 'Controller_0_00010101'
    ctx.instance:on_add_object('ControllerFirmware', { ComponentID = 1, ComponentIDEx = 100 }, position)
    lu.assertNotNil(ctx.instance.controller_firmware_objs[position])

    -- Now delete it
    ctx.instance:on_delete_object('ControllerFirmware', {}, position)
    lu.assertNil(ctx.instance.controller_firmware_objs[position])
end

function TestControllerFirmwareObject:test_on_delete_object_wrong_class_ignored()
    local log_capture = {}
    local ctx = setup_mocked_firmware_object(log_capture, nil)

    -- Should return early without error
    ctx.instance:on_delete_object('NotControllerFirmware', {}, 'pos')

    -- No crash, no warn
    local found_warn = false
    for _, entry in ipairs(log_capture) do
        if entry.level == 'warning' then
            found_warn = true
            break
        end
    end
    lu.assertFalse(found_warn, 'Should not log warning for non-ControllerFirmware class')
end

function TestControllerFirmwareObject:test_on_delete_object_nonexistent_position()
    local log_capture = {}
    local ctx = setup_mocked_firmware_object(log_capture, nil)

    -- Deleting non-existent position should not crash
    ctx.instance:on_delete_object('ControllerFirmware', {}, 'nonexistent_position')
    lu.assertNil(ctx.instance.controller_firmware_objs['nonexistent_position'])
end

-- ============================
-- Tests: find_upgradable_raid_firmware_obj
-- ============================

function TestControllerFirmwareObject:test_find_upgradable_raid_firmware_obj_match()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 1,
        Id = 0,
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    local position = 'Controller_0_00010101'
    ctx.instance:on_add_object('ControllerFirmware', { ComponentID = 1, ComponentIDEx = 100 }, position)

    local result = ctx.instance:find_upgradable_raid_firmware_obj(1, 100)
    lu.assertEquals(#result, 1)
    lu.assertEquals(result[1].ComponentID, 1)
    lu.assertEquals(result[1].ComponentIDEx, 100)
    lu.assertEquals(result[1].Position, position)
    lu.assertEquals(result[1].ref_controller.Id, 0)
end

function TestControllerFirmwareObject:test_find_upgradable_raid_firmware_obj_no_oob()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 0, -- not OOB
        Id = 0,
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    ctx.instance:on_add_object('ControllerFirmware', { ComponentID = 1, ComponentIDEx = 100 }, 'pos1')

    local result = ctx.instance:find_upgradable_raid_firmware_obj(1, 100)
    lu.assertEquals(#result, 0)
end

function TestControllerFirmwareObject:test_find_upgradable_raid_firmware_obj_no_match()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 1,
        Id = 0,
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    ctx.instance:on_add_object('ControllerFirmware', { ComponentID = 1, ComponentIDEx = 100 }, 'pos1')

    -- Different component_id
    local result = ctx.instance:find_upgradable_raid_firmware_obj(2, 100)
    lu.assertEquals(#result, 0)
end

function TestControllerFirmwareObject:test_find_upgradable_raid_firmware_obj_empty()
    local log_capture = {}
    local ctx = setup_mocked_firmware_object(log_capture, nil)

    local result = ctx.instance:find_upgradable_raid_firmware_obj(1, 100)
    lu.assertEquals(#result, 0)
end

-- ============================
-- Tests: has_valid_upgradable_raid
-- ============================

function TestControllerFirmwareObject:test_has_valid_upgradable_raid_true()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 1,
        registered_state = true,
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    ctx.instance:on_add_object('ControllerFirmware', { ComponentID = 1, ComponentIDEx = 100 }, 'pos1')

    lu.assertTrue(ctx.instance:has_valid_upgradable_raid())
end

function TestControllerFirmwareObject:test_has_valid_upgradable_raid_false_no_oob()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 0,
        registered_state = true,
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    ctx.instance:on_add_object('ControllerFirmware', { ComponentID = 1, ComponentIDEx = 100 }, 'pos1')

    lu.assertFalse(ctx.instance:has_valid_upgradable_raid())
end

function TestControllerFirmwareObject:test_has_valid_upgradable_raid_false_not_registered()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 1,
        registered_state = false,
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    ctx.instance:on_add_object('ControllerFirmware', { ComponentID = 1, ComponentIDEx = 100 }, 'pos1')

    lu.assertFalse(ctx.instance:has_valid_upgradable_raid())
end

function TestControllerFirmwareObject:test_has_valid_upgradable_raid_empty()
    local log_capture = {}
    local ctx = setup_mocked_firmware_object(log_capture, nil)

    lu.assertFalse(ctx.instance:has_valid_upgradable_raid())
end

-- ============================
-- Tests: get_cfg_match_controller_version
-- ============================

function TestControllerFirmwareObject:test_get_cfg_match_controller_version_found()
    local log_capture = {}
    local mock_controller = {
        DeviceName = 'TestController',
        OOBSupport = 1,
        Id = 0,
        FirmwareVersion = '1.0.0',
    }
    local ctx = setup_mocked_firmware_object(log_capture, mock_controller)

    ctx.instance:on_add_object('ControllerFirmware', { ComponentID = 1, ComponentIDEx = 100 }, 'pos1')

    local result = ctx.instance:get_cfg_match_controller_version(1, 100)
    lu.assertEquals(result, '1.0.0')
end

function TestControllerFirmwareObject:test_get_cfg_match_controller_version_not_found()
    local log_capture = {}
    local ctx = setup_mocked_firmware_object(log_capture, nil)

    local result = ctx.instance:get_cfg_match_controller_version(1, 100)
    lu.assertEquals(result, '')
end