-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- See the Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2

local lu = require('luaunit')
local hook_orm_tasks = require('hook_orm_tasks')

-- 可注入的 stub 控制
local stub_storageconfig = {
    power_on = 1,
    obj = {
        PhyErrorEnabled = true,
        FileMockEnabled = false,
        PhyErrorInterval = 60,
        PhyErrorThreshold = 10,
        MaxPhyErrorLogFileRotationCount = 5,
        PhyErrorMaxRecord = 100,
        PhyErrorSelUnlimitFlag = 0,
    },
    support_oob = function()
        return true
    end,
}

local stub_controllers = {}
local stub_controller_by_id = {}

local stub_sml_phy_err = nil -- nil 表示 pcall 失败,否则返回 phy 数组
local stub_add_event_generate = nil

local stub_utils_core_is_dir = true
local stub_utils_realpath = nil -- nil 表示文件不存在
local stub_utils_mkdir = true
local stub_file_open = nil -- nil 表示打开失败,否则返回 mock file
local stub_file_content = ''

TestPhyBiterr = {}

function TestPhyBiterr:setUp()
    package.loaded['diagnose.phy_biterr'] = nil
    package.loaded['mc.orm.tasks'] = nil
    stub_storageconfig.power_on = 1
    stub_storageconfig.obj = {
        PhyErrorEnabled = true,
        FileMockEnabled = false,
        PhyErrorInterval = 60,
        PhyErrorThreshold = 10,
        MaxPhyErrorLogFileRotationCount = 5,
        PhyErrorMaxRecord = 100,
        PhyErrorSelUnlimitFlag = 0,
    }
    stub_storageconfig.support_oob = function()
        return true
    end
    stub_controllers = {}
    stub_controller_by_id = {}
    stub_sml_phy_err = nil
    stub_add_event_generate = nil
    stub_utils_core_is_dir = true
    stub_utils_realpath = nil
    stub_utils_mkdir = true
    stub_file_open = nil
    stub_file_content = ''

    hook_orm_tasks.hook()
    local mc_orm_tasks = require('mc.orm.tasks')
    local orig_new = mc_orm_tasks.new
    self._orig_tasks_new = orig_new
    mc_orm_tasks.new = function()
        local inst = orig_new()
        inst.timeout_ms = function(_, _ms, fn)
            fn()
        end
        inst.sleep_ms = function(_, _ms) end
        inst.set_timeout_ms = function()
            return inst
        end
        return inst
    end

    package.preload['storageconfig.storageconfig_object'] = function()
        return {
            get_instance = function()
                return stub_storageconfig
            end,
        }
    end
    package.preload['controller.controller_collection'] = function()
        return {
            get_instance = function()
                return {
                    get_all_controllers = function()
                        return stub_controllers
                    end,
                    get_by_controller_id = function(_, id)
                        return stub_controller_by_id[id]
                    end,
                }
            end,
        }
    end
    package.preload['add_event'] = function()
        return {
            get_instance = function()
                return {
                    generate_phy_increase_too_fast = function(_, _desc)
                        if stub_add_event_generate then
                            stub_add_event_generate()
                        end
                    end,
                }
            end,
        }
    end
    package.preload['common_def'] = function()
        return { INVALID_U8 = 0xff, INVALID_U32 = 0xffffffff }
    end
    package.preload['utils.core'] = function()
        return {
            is_dir = function()
                return stub_utils_core_is_dir
            end,
        }
    end
    package.preload['utils.file'] = function()
        return {
            open_s = function(path, mode)
                if stub_file_open == false then
                    return nil
                end
                if type(stub_file_open) == 'function' then
                    return stub_file_open(path, mode)
                end
                local content = stub_file_content
                return {
                    read = function(_, fmt)
                        return content
                    end,
                    write = function(_, s)
                        content = (content or '') .. (s or '')
                    end,
                    close = function() end,
                }
            end,
        }
    end
    package.preload['sml'] = function()
        return {
            get_ctrl_sas_phy_err = function(ctrl_idx, force)
                if stub_sml_phy_err == nil then
                    error('sml stub error')
                end
                return stub_sml_phy_err
            end,
        }
    end

    -- 这些模块可能已被其他用例加载,用 preload 覆盖下次 require
    package.loaded['storageconfig.storageconfig_object'] = nil
    package.loaded['controller.controller_collection'] = nil
    package.loaded['add_event'] = nil
    package.loaded['common_def'] = nil
    package.loaded['utils.core'] = nil
    package.loaded['utils.file'] = nil
    package.loaded['sml'] = nil
end

function TestPhyBiterr:tearDown()
    if self._orig_tasks_new then
        local mc_orm_tasks = require('mc.orm.tasks')
        mc_orm_tasks.new = self._orig_tasks_new
    end
    hook_orm_tasks.unhook()
    package.loaded['diagnose.phy_biterr'] = nil
    package.loaded['mc.orm.tasks'] = nil
    package.loaded['storageconfig.storageconfig_object'] = nil
    package.loaded['controller.controller_collection'] = nil
    package.loaded['add_event'] = nil
    package.loaded['common_def'] = nil
    package.loaded['utils.core'] = nil
    package.loaded['utils.file'] = nil
    package.loaded['sml'] = nil
    package.preload['storageconfig.storageconfig_object'] = nil
    package.preload['controller.controller_collection'] = nil
    package.preload['add_event'] = nil
    package.preload['common_def'] = nil
    package.preload['utils.core'] = nil
    package.preload['utils.file'] = nil
    package.preload['sml'] = nil
end

function TestPhyBiterr:test_get_instance_and_ctor()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    lu.assertNotNil(inst)
    lu.assertNotNil(inst.diagnose_list)
    lu.assertNotNil(inst.on_controller_commu_changed)
    lu.assertEquals(inst.phy_biterr_last_diag_time, 0)
end

-- 覆盖 init:on_controller_commu_changed 回调中 mctp_state 为 true 时调用 start
function TestPhyBiterr:test_init_callback_starts_when_mctp_state_true()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local start_called = false
    inst.start = function()
        start_called = true
    end
    inst:init()
    inst.on_controller_commu_changed:emit(true)
    lu.assertTrue(start_called)
end

function TestPhyBiterr:test_init_diag_info_data_invalid_cnt()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local res = inst:init_diag_info_data('invalid_cnt')
    lu.assertNotNil(res)
    lu.assertEquals(res[0], 0)
    lu.assertEquals(res[99], 0)
end

function TestPhyBiterr:test_init_diag_info_data_err_status()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local data = 1 << 30 | 1 << 29 | 1 << 28
    local res = inst:init_diag_info_data('err_status', data)
    lu.assertNotNil(res)
    lu.assertEquals(res[0].err_cnt_changed, data & (1 << 30))
    lu.assertEquals(res[0].err_cnt_change_dir, data & (1 << 29))
    lu.assertEquals(res[0].increased_over_thresh, data & (1 << 28))
end

function TestPhyBiterr:test_init_diag_info_data_err_cnt_info()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local res = inst:init_diag_info_data('err_cnt_info')
    lu.assertNotNil(res)
    lu.assertEquals(res[0].phy_id, 0)
    lu.assertEquals(res[0].invalid_dword_count, 0)
    lu.assertEquals(res[0].loss_dword_sync_count, 0)
    lu.assertEquals(res[0].phy_reset_problem_count, 0)
    lu.assertEquals(res[0].running_disparity_error_count, 0)
end

function TestPhyBiterr:test_generate_new_diag_info()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local info = inst:generate_new_diag_info()
    lu.assertNotNil(info.raid_phy)
    lu.assertNotNil(info.raid_phy.err_status)
    lu.assertNotNil(info.raid_phy.err_cnt_info)
    lu.assertNotNil(info.raid_phy.invalid_cnt)
    lu.assertNotNil(info.raid_mock_mgnt_info)
    lu.assertEquals(info.raid_mock_mgnt_info.mock_first_time, true)
end

function TestPhyBiterr:test_diagnose_init_empty_controllers()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.diagnose_list = {}
    stub_controllers = {}
    inst:diagnose_init()
    lu.assertEquals(#inst.diagnose_list, 0)
end

function TestPhyBiterr:test_diagnose_init_skip_soft_raid()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.diagnose_list = {}
    stub_controllers = {
        {
            TypeId = 0xC0,
            Id = 1,
            get_phy_children = function()
                return { 1, 2 }
            end,
        }, -- SM_SOFT_RAID
    }
    inst:diagnose_init()
    lu.assertEquals(#inst.diagnose_list, 0)
end

function TestPhyBiterr:test_diagnose_init_add_controller()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.diagnose_list = {}
    stub_controllers = {
        {
            TypeId = 0,
            Id = 10,
            get_phy_children = function()
                return { 1, 2, 3 }
            end,
        },
    }
    inst:diagnose_init()
    lu.assertEquals(#inst.diagnose_list, 1)
    lu.assertEquals(inst.diagnose_list[1].obj_index, 10)
    lu.assertEquals(inst.diagnose_list[1].max_raid_phy_count, 3)
end

function TestPhyBiterr:test_diagnose_init_pcall_fail()
    package.loaded['diagnose.phy_biterr'] = nil
    package.preload['controller.controller_collection'] = function()
        return {
            get_instance = function()
                return {
                    get_all_controllers = function()
                        error('stub')
                    end,
                    get_by_controller_id = function() end,
                }
            end,
        }
    end
    package.loaded['controller.controller_collection'] = nil
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.diagnose_list = {}
    inst:diagnose_init()
    lu.assertEquals(#inst.diagnose_list, 0)
end

function TestPhyBiterr:test_check_phy_biterr_diagnose_startup_first_time()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.phy_biterr_last_diag_time = 0
    inst.config_obj = { PhyErrorInterval = 60 }
    lu.assertTrue(inst:check_phy_biterr_diagnose_startup())
end

function TestPhyBiterr:test_check_phy_biterr_diagnose_startup_interval_not_reached()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.phy_biterr_last_diag_time = os.time() - 30
    inst.config_obj = { PhyErrorInterval = 60 }
    lu.assertFalse(inst:check_phy_biterr_diagnose_startup())
end

function TestPhyBiterr:test_check_phy_biterr_diagnose_startup_interval_reached()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.phy_biterr_last_diag_time = os.time() - 100
    inst.config_obj = { PhyErrorInterval = 60 }
    lu.assertTrue(inst:check_phy_biterr_diagnose_startup())
end

function TestPhyBiterr:test_phy_biterr_compare_nil_args()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config_obj = { PhyErrorThreshold = 10 }
    local p_status = {}
    inst:phy_biterr_compare(nil, {}, 0, p_status)
    inst:phy_biterr_compare({}, {}, nil, p_status)
    inst:phy_biterr_compare({}, {}, 0, nil)
end

function TestPhyBiterr:test_phy_biterr_compare_delta_and_thresh()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config_obj = { PhyErrorThreshold = 10 }
    local p_last = {
        phy_id = 0,
        invalid_dword_count = 0,
        loss_dword_sync_count = 0,
        phy_reset_problem_count = 0,
        running_disparity_error_count = 0,
    }
    local p_current = {
        phy_id = 0,
        invalid_dword_count = 100,
        loss_dword_sync_count = 0,
        phy_reset_problem_count = 0,
        running_disparity_error_count = 0,
    }
    local p_status = { err_cnt_changed = 0, err_cnt_change_dir = 0, increased_over_thresh = 0 }
    inst:phy_biterr_compare(p_last, p_current, 0, p_status)
    lu.assertEquals(p_status.err_cnt_changed, 1)
    lu.assertEquals(p_status.err_cnt_change_dir, 0)
    lu.assertEquals(p_status.increased_over_thresh, 1)
end

function TestPhyBiterr:test_phy_biterr_compare_calculate_interval_zero()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config_obj = { PhyErrorThreshold = 10 }
    local p_last = {
        phy_id = 0,
        invalid_dword_count = 0,
        loss_dword_sync_count = 0,
        phy_reset_problem_count = 0,
        running_disparity_error_count = 0,
    }
    local p_current = {
        phy_id = 0,
        invalid_dword_count = 5,
        loss_dword_sync_count = 0,
        phy_reset_problem_count = 0,
        running_disparity_error_count = 0,
    }
    local p_status = { err_cnt_changed = 0, err_cnt_change_dir = 0, increased_over_thresh = 1 }
    inst:phy_biterr_compare(p_last, p_current, -1, p_status)
    lu.assertEquals(p_status.increased_over_thresh, 0)
end

function TestPhyBiterr:test_phy_biterr_check_valid()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local p_last = {
        invalid_dword_count = 0,
        loss_dword_sync_count = 0,
        phy_reset_problem_count = 0,
        running_disparity_error_count = 0,
    }
    local p_current = {
        invalid_dword_count = 1,
        loss_dword_sync_count = 1,
        phy_reset_problem_count = 0,
        running_disparity_error_count = 0,
    }
    local invalid_cnt = 0
    lu.assertTrue(inst:phy_biterr_check(p_last, p_current, invalid_cnt))
end

function TestPhyBiterr:test_phy_biterr_check_invalid_decrease()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local p_last = {
        phy_id = 0,
        invalid_dword_count = 10,
        loss_dword_sync_count = 10,
        phy_reset_problem_count = 0,
        running_disparity_error_count = 0,
    }
    local p_current = {
        invalid_dword_count = 5,
        loss_dword_sync_count = 5,
        phy_reset_problem_count = 0,
        running_disparity_error_count = 0,
    }
    local invalid_cnt = 0
    lu.assertFalse(inst:phy_biterr_check(p_last, p_current, invalid_cnt))
end

function TestPhyBiterr:test_phy_biterr_get_failed_cnt_nil()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst:phy_biterr_get_failed_cnt(nil)
end

function TestPhyBiterr:test_phy_biterr_get_failed_cnt_base_obtained()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local p_phyerr = {
        raid_phy = { base_data_obtained = 1, failed_cnt = 2 },
    }
    inst:phy_biterr_get_failed_cnt(p_phyerr)
    lu.assertEquals(p_phyerr.raid_phy.failed_cnt, 3)
end

function TestPhyBiterr:test_phy_biterr_get_failed_cnt_base_not_obtained()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local p_phyerr = {
        raid_phy = { base_data_obtained = false, failed_cnt = 5 },
    }
    inst:phy_biterr_get_failed_cnt(p_phyerr)
    lu.assertEquals(p_phyerr.raid_phy.failed_cnt, 0)
end

function TestPhyBiterr:test_phy_biterr_record_raid_nil()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst:phy_biterr_record_raid(nil)
end

function TestPhyBiterr:test_phy_biterr_record_raid_pcall_fail()
    package.loaded['diagnose.phy_biterr'] = nil
    package.preload['controller.controller_collection'] = function()
        return {
            get_instance = function()
                return {
                    get_all_controllers = function()
                        return {}
                    end,
                    get_by_controller_id = function()
                        error('stub')
                    end,
                }
            end,
        }
    end
    package.loaded['controller.controller_collection'] = nil
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local p_phyerr = { obj_index = 1, raid_phy = {} }
    inst:phy_biterr_record_raid(p_phyerr)
end

function TestPhyBiterr:test_phy_biterr_sel_generate_nil()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst:phy_biterr_sel_generate(nil)
end

function TestPhyBiterr:test_phy_biterr_sel_generate_pcall_fail()
    package.loaded['diagnose.phy_biterr'] = nil
    package.preload['controller.controller_collection'] = function()
        return {
            get_instance = function()
                return {
                    get_all_controllers = function()
                        return {}
                    end,
                    get_by_controller_id = function()
                        error('stub')
                    end,
                }
            end,
        }
    end
    package.loaded['controller.controller_collection'] = nil
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config_obj = { PhyErrorSelUnlimitFlag = 0 }
    local p_phyerr = {
        obj_index = 1,
        raid_phy = {
            base_data_obtained = 1,
            phy_count = 1,
            err_status = { [0] = { increased_over_thresh = 1, sel_reported = 0 } },
        },
    }
    inst:phy_biterr_sel_generate(p_phyerr)
end

function TestPhyBiterr:test_phy_biterr_sel_generate_increased_over_thresh()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config_obj = { PhyErrorSelUnlimitFlag = 0 }
    stub_controller_by_id[1] = { DeviceName = 'RAID1' }
    local generated = false
    stub_add_event_generate = function()
        generated = true
    end
    local p_phyerr = {
        obj_index = 1,
        raid_phy = {
            base_data_obtained = 1,
            phy_count = 1,
            err_status = { [0] = { increased_over_thresh = 1, sel_reported = 0 } },
        },
    }
    inst:phy_biterr_sel_generate(p_phyerr)
    lu.assertTrue(generated)
    lu.assertEquals(p_phyerr.raid_phy.err_status[0].sel_reported, 1)
end

function TestPhyBiterr:test_phy_biterr_analyze_raid_first_valid()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config_obj = stub_storageconfig.obj
    local pre_info = inst:generate_new_diag_info()
    pre_info.obj_index = 0
    pre_info.raid_phy.base_data_obtained = 0
    pre_info.raid_phy.phy_count = 0
    local cur_info = inst:generate_new_diag_info()
    cur_info.raid_phy.phy_count = 2
    cur_info.raid_phy.err_cnt_info[0].phy_id = 0
    cur_info.raid_phy.err_cnt_info[1].phy_id = 1
    inst:phy_biterr_analyze_raid(pre_info, cur_info)
    lu.assertEquals(pre_info.raid_phy.base_data_obtained, 1)
    lu.assertEquals(pre_info.raid_phy.first_valid_data, 1)
    lu.assertEquals(pre_info.raid_phy.phy_count, 2)
end

function TestPhyBiterr:test_phy_biterr_analyze_raid_phy_count_zero()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local pre_info = inst:generate_new_diag_info()
    pre_info.raid_phy.base_data_obtained = 1
    pre_info.raid_phy.failed_cnt = 0
    local cur_info = inst:generate_new_diag_info()
    cur_info.raid_phy.phy_count = 0
    inst:phy_biterr_analyze_raid(pre_info, cur_info)
    lu.assertEquals(pre_info.raid_phy.failed_cnt, 1)
end

function TestPhyBiterr:test_get_ctrl_sas_phy_err_count_fail()
    package.loaded['diagnose.phy_biterr'] = nil
    stub_sml_phy_err = nil
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local ctrl_phy_err = { i_controller_index = 0, i_force_update = 1 }
    local ret = inst:get_ctrl_sas_phy_err_count(ctrl_phy_err)
    lu.assertEquals(ret, -1)
end

function TestPhyBiterr:test_get_ctrl_sas_phy_err_count_ok()
    stub_sml_phy_err = {
        [0] = {
            invalid_dword_count = 1,
            loss_dword_sync_count = 2,
            phy_reset_problem_count = 0,
            running_disparity_error_count = 3,
        },
    }
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local ctrl_phy_err = { i_controller_index = 0, i_force_update = 1 }
    local ret = inst:get_ctrl_sas_phy_err_count(ctrl_phy_err)
    lu.assertEquals(ret, 0)
    lu.assertEquals(ctrl_phy_err.o_phy_count, 1)
    lu.assertEquals(ctrl_phy_err.o_phy[0].invalid_dword_count, 1)
    lu.assertEquals(ctrl_phy_err.o_phy[0].running_disparity_error_count, 3)
end

function TestPhyBiterr:test_phy_biterr_get_from_oob_ok()
    stub_sml_phy_err = {
        [0] = {
            invalid_dword_count = 0,
            loss_dword_sync_count = 0,
            phy_reset_problem_count = 0,
            running_disparity_error_count = 0,
        },
    }
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local diag_info = inst:generate_new_diag_info()
    diag_info.obj_index = 0
    diag_info.max_raid_phy_count = 4
    local ret = inst:phy_biterr_get_from_oob(diag_info)
    lu.assertEquals(ret, 0)
    lu.assertEquals(diag_info.raid_phy.phy_count, 1)
end

function TestPhyBiterr:test_phy_biterr_get_file_mock_enabled()
    stub_storageconfig.obj.FileMockEnabled = true
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config = stub_storageconfig
    inst.config_obj = stub_storageconfig.obj
    local pre = inst:generate_new_diag_info()
    pre.obj_index = 0
    pre.raid_mock_mgnt_info.file_name = '/nonexistent_phy_mock_file_12345'
    pre.raid_mock_mgnt_info.mock_first_time = true
    local cur = inst:generate_new_diag_info()
    local ret = inst:phy_biterr_get(pre, cur)
    lu.assertTrue(ret == true)
end

function TestPhyBiterr:test_phy_biterr_process_no_config_obj()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config = stub_storageconfig
    inst.config_obj = nil
    inst.diagnose_list = {}
    inst:phy_biterr_process()
    lu.assertEquals(inst.phy_biterr_last_diag_time, 0)
end

function TestPhyBiterr:test_phy_biterr_process_phy_error_disabled()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config = stub_storageconfig
    inst.config_obj = { PhyErrorEnabled = false }
    inst.diagnose_list = {}
    inst:phy_biterr_process()
    lu.assertEquals(inst.phy_biterr_last_diag_time, 0)
end

function TestPhyBiterr:test_phy_biterr_process_support_oob_false()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    stub_storageconfig.support_oob = function()
        return false
    end
    inst.config = stub_storageconfig
    inst.config_obj = { PhyErrorEnabled = true }
    inst.diagnose_list = {}
    inst:phy_biterr_process()
    lu.assertEquals(inst.phy_biterr_last_diag_time, 0)
end

function TestPhyBiterr:test_phy_biterr_process_diagnose_startup_false()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config = stub_storageconfig
    inst.config_obj = { PhyErrorEnabled = true }
    inst.phy_biterr_last_diag_time = os.time() - 10
    inst.config_obj.PhyErrorInterval = 60
    inst.diagnose_list = {}
    inst:phy_biterr_process()
    lu.assertEquals(inst.phy_biterr_last_diag_time, os.time() - 10)
end

function TestPhyBiterr:test_mock_controller_not_exist()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.diagnose_list = {}
    local ret = inst:mock(99, nil, '/path')
    lu.assertEquals(ret, -1)
end

function TestPhyBiterr:test_mock_ok()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local diag = inst:generate_new_diag_info()
    diag.obj_index = 10
    inst.diagnose_list = { diag }
    local ret = inst:mock(10, nil, '/tmp/mock.csv')
    lu.assertEquals(ret, 0)
    lu.assertEquals(diag.raid_mock_mgnt_info.mock_enabled, 1)
    lu.assertEquals(diag.raid_mock_mgnt_info.file_name, '/tmp/mock.csv')
    lu.assertTrue(diag.raid_mock_mgnt_info.mock_first_time)
end

function TestPhyBiterr:test_phy_log_write_header_nil_fp()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    stub_controller_by_id[0] = { DeviceName = 'Ctrl0', SerialNumber = 'SN0' }
    inst:phy_log_write_header(nil, { obj_index = 0 }, 0)
    inst:phy_log_write_header({}, nil, 0)
end

function TestPhyBiterr:test_phy_log_write_base_data_nil()
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst:phy_log_write_base_data(nil, { raid_phy = { phy_count = 1 } }, 0)
    inst:phy_log_write_base_data({ write = function() end }, nil, 0)
end

function TestPhyBiterr:test_phy_log_get_record_num()
    stub_file_content = '0,1,2,3,4\n0,2,3,4,5\n1,0,0,0,0\n'
    stub_file_open = true
    package.preload['utils.file'] = function()
        return {
            open_s = function()
                return {
                    read = function(_, fmt)
                        return stub_file_content
                    end,
                    close = function() end,
                }
            end,
        }
    end
    package.loaded['utils.file'] = nil
    package.loaded['diagnose.phy_biterr'] = nil
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local num = inst:phy_log_get_record_num('/tmp/x.csv', 0)
    lu.assertNotNil(num)
    lu.assertEquals(num, 2)
end

function TestPhyBiterr:test_phy_log_get_max_phy_id()
    stub_file_content = '0,1,2,3,4\n1,0,0,0,0\n2,0,0,0,0\n'
    package.preload['utils.file'] = function()
        return {
            open_s = function()
                return {
                    read = function(_, fmt)
                        return stub_file_content
                    end,
                    close = function() end,
                }
            end,
        }
    end
    package.loaded['utils.file'] = nil
    package.loaded['diagnose.phy_biterr'] = nil
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local max_id = inst:phy_log_get_max_phy_id('/tmp/x.csv')
    lu.assertEquals(max_id, 2)
end

function TestPhyBiterr:test_phy_log_get_max_phy_id_empty()
    stub_file_content = ''
    package.preload['utils.file'] = function()
        return {
            open_s = function()
                return {
                    read = function(_, fmt)
                        return ''
                    end,
                    close = function() end,
                }
            end,
        }
    end
    package.loaded['utils.file'] = nil
    package.loaded['diagnose.phy_biterr'] = nil
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    local max_id = inst:phy_log_get_max_phy_id('/tmp/empty.csv')
    lu.assertEquals(max_id, 0xff)
end

-- 覆盖 start:power_on == 0 时清零分支 (line 140)
function TestPhyBiterr:test_start_power_off_clears_state()
    stub_storageconfig.power_on = 0
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.phy_biterr_last_diag_time = 12345
    inst.diagnose_list = { { obj_index = 1 } }
    -- hook mc.orm.tasks 的 loop,使回调存储后手动执行
    local c_tasks = require('mc.orm.tasks')
    local my_tasks = c_tasks.get_instance()
    local orig_loop = my_tasks.c_task.loop
    my_tasks.c_task.loop = function(self_t, cb)
        self_t.loop_cb = cb
        return self_t
    end
    inst:start()
    local task = my_tasks:get_task('phy_biterr_update')
    if task and task.loop_cb then
        task:run_task(task.loop_cb)
    end
    my_tasks.c_task.loop = orig_loop
    lu.assertEquals(inst.phy_biterr_last_diag_time, 0)
    lu.assertEquals(#inst.diagnose_list, 0)
    stub_storageconfig.power_on = 1
end

-- 覆盖 phy_biterr_record_raid:mkdir 失败分支 (line 224)
function TestPhyBiterr:test_phy_biterr_record_raid_mkdir_failed()
    stub_utils_core_is_dir = false
    package.preload['utils.core'] = function()
        return {
            is_dir = function()
                return stub_utils_core_is_dir
            end,
        }
    end
    package.preload['utils.file'] = function()
        return {
            open_s = function()
                return nil
            end,
        }
    end
    package.preload['utils'] = function()
        return {
            S_IRWXU = 448,
            S_IRGRP = 32,
            S_IXGRP = 16,
            S_IROTH = 4,
            S_IXOTH = 1,
            mkdir_with_parents = function()
                return false
            end,
            realpath = function()
                return nil
            end,
        }
    end
    package.loaded['utils.core'] = nil
    package.loaded['utils.file'] = nil
    package.loaded['utils'] = nil
    package.loaded['diagnose.phy_biterr'] = nil
    local phy_biterr = require('diagnose.phy_biterr')
    local inst = phy_biterr.get_instance()
    inst.config_obj = stub_storageconfig.obj
    stub_controller_by_id[1] = { DeviceName = 'RAID1' }
    local p_phyerr = {
        obj_index = 1,
        raid_phy = { first_valid_data = 1 },
    }
    inst:phy_biterr_record_raid(p_phyerr)
    lu.assertTrue(true)
end