-- 调试向 UT:get_locating_led_snapshot / del_pd 对称 / identify_task 边界 / pd_object LOCATE 失败
-- 须在 test_pd_identify_service 之前 require(LuaUnit 按类名排序 Debug 先于 Service)

local c_tasks = require('mc.orm.tasks')
if not c_tasks._pd_ut_sleep_patch then
    c_tasks._pd_ut_sleep_patch = true
    local raw_get_instance = c_tasks.get_instance
    local fallback_tasks = { sleep_ms = function() end }
    c_tasks.get_instance = function()
        local inst = raw_get_instance()
        return inst or fallback_tasks
    end
end

local pd_service = require('pd.pd_identify_service')
local pd_object = require('pd.pd_object')
local c_storageconfig = require('storageconfig.storageconfig_object')
local drive_collection = require('drive.drive_collection')
local lu = require('luaunit')
local skynet = require('skynet')
local sml = require('sml')

local PD_MAX_TRY <const> = 60

Test_Pd_Identify_Debug = {}

function Test_Pd_Identify_Debug:setupClass()
    pd_service.new(nil)
end

function Test_Pd_Identify_Debug:setUp()
    if
        type(pd_service.drive_list) ~= 'table'
        or type(pd_service.pd_list) ~= 'table'
        or type(pd_service.identified_list) ~= 'table'
    then
        pd_service.new(nil)
    end
end

local function clone_list(t)
    local r = {}
    if not t then
        return r
    end
    for i, v in ipairs(t) do
        r[i] = v
    end
    return r
end

local function snapshot_pd_lists()
    return {
        drive_list = clone_list(pd_service.drive_list),
        pd_list = clone_list(pd_service.pd_list),
        identified_list = clone_list(pd_service.identified_list),
    }
end

local function restore_pd_lists(s)
    pd_service.drive_list = clone_list(s.drive_list)
    pd_service.pd_list = clone_list(s.pd_list)
    pd_service.identified_list = clone_list(s.identified_list)
end

function Test_Pd_Identify_Debug:test_get_locating_led_snapshot_only_pending_locating()
    local snap = snapshot_pd_lists()
    local d_on = {
        Name = 'Disk_on',
        is_presence = function()
            return true
        end,
        is_being_located = function()
            return true
        end,
    }
    local d_off = {
        Name = 'Disk_off',
        is_presence = function()
            return true
        end,
        is_being_located = function()
            return false
        end,
    }
    pd_service.drive_list = { d_on, d_off }
    local cnt, names = pd_service:get_locating_led_snapshot()
    lu.assertEquals(cnt, 1)
    lu.assertEquals(names[1], 'Disk_on')
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_del_pd_calls_identified_nil_before_re_add()
    local snap = snapshot_pd_lists()
    local nil_identified_count = 0
    local drive = {
        Name = 'Disk_sym',
        identified = function(_, arg)
            if arg == nil then
                nil_identified_count = nil_identified_count + 1
            end
        end,
    }
    local pd = {
        key = 'pd_sym:1:1:1',
        is_valid = function()
            return true
        end,
    }
    pd_service.identified_list = { { pd = pd, drive = drive } }
    pd_service.pd_list = {}
    local old_add = pd_service.add_drive
    local readded_name
    pd_service.add_drive = function(_, d)
        readded_name = d.Name
    end
    pd_service:del_pd(pd)
    pd_service.add_drive = old_add
    lu.assertEquals(nil_identified_count, 1)
    lu.assertEquals(readded_name, 'Disk_sym')
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_identify_task_returns_false_when_no_pending_drives()
    local snap = snapshot_pd_lists()
    pd_service.drive_list = {}
    pd_service.pd_list = {
        {
            key = 'p',
            is_valid = function()
                return true
            end,
        },
    }
    lu.assertIsFalse(pd_service:identify_task(0))
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_identify_task_returns_true_when_no_pending_pds()
    local snap = snapshot_pd_lists()
    pd_service.drive_list = { { Name = 'D' } }
    pd_service.pd_list = {}
    lu.assertIsTrue(pd_service:identify_task(0))
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_identify_task_locate_start_fail_smoke()
    local snap = snapshot_pd_lists()
    local old_sk_sleep = skynet.sleep
    local old_idpd = pd_service.identify_pd_by_persistence_data
    ---@diagnostic disable-next-line: duplicate-set-field
    skynet.sleep = function() end
    pd_service.identify_pd_by_persistence_data = function()
        return false
    end
    local pd = {
        key = 'pd_loc_fail',
        is_valid = function()
            return true
        end,
        locate = function()
            return {
                start = function()
                    return false
                end,
                __close = function() end,
            }
        end,
    }
    pd_service.drive_list = {
        {
            Name = 'Disk_loc',
            is_presence = function()
                return true
            end,
            is_being_located = function()
                return true
            end,
        },
    }
    pd_service.pd_list = { pd }
    local r59 = pd_service:identify_task(59)
    local r60 = pd_service:identify_task(60)
    skynet.sleep = old_sk_sleep
    pd_service.identify_pd_by_persistence_data = old_idpd
    restore_pd_lists(snap)
    lu.assertIsTrue(r59)
    lu.assertIsTrue(r60)
end

function Test_Pd_Identify_Debug:test_identify_task_verbose_pd_invalid_after_locate_settle()
    local snap = snapshot_pd_lists()
    local old_sk = skynet.sleep
    local old_idpd = pd_service.identify_pd_by_persistence_data
    pd_service.identify_pd_by_persistence_data = function()
        return false
    end
    local valid = true
    local pd = {
        key = 'pd_inv_settle',
        is_valid = function()
            return valid
        end,
        locate = function()
            return {
                start = function()
                    return true
                end,
                __close = function() end,
            }
        end,
    }
    skynet.sleep = function()
        valid = false
    end
    pd_service.drive_list = {
        {
            Name = 'D_settle',
            is_presence = function()
                return true
            end,
            is_being_located = function()
                return true
            end,
        },
    }
    pd_service.pd_list = { pd }
    lu.assertIsTrue(pd_service:identify_task(PD_MAX_TRY))
    skynet.sleep = old_sk
    pd_service.identify_pd_by_persistence_data = old_idpd
    restore_pd_lists(snap)
end

--- sleep 后 pd 仍有效但已从 pd_list 移除(find_pd_list 为 nil,走 538-541 另一分支)
function Test_Pd_Identify_Debug:test_identify_task_verbose_pd_removed_from_pd_list_after_sleep()
    local snap = snapshot_pd_lists()
    local old_sk = skynet.sleep
    local old_idpd = pd_service.identify_pd_by_persistence_data
    pd_service.identify_pd_by_persistence_data = function()
        return false
    end
    local pd = {
        key = 'pd_rm_list',
        is_valid = function()
            return true
        end,
        locate = function()
            return {
                start = function()
                    return true
                end,
                __close = function() end,
            }
        end,
    }
    local other = {
        key = 'pd_other_only',
        is_valid = function()
            return true
        end,
    }
    skynet.sleep = function()
        pd_service.pd_list = { other }
    end
    pd_service.drive_list = {
        {
            Name = 'D_rm_list',
            is_presence = function()
                return true
            end,
            is_being_located = function()
                return true
            end,
        },
    }
    pd_service.pd_list = { pd }
    lu.assertIsTrue(pd_service:identify_task(PD_MAX_TRY))
    skynet.sleep = old_sk
    pd_service.identify_pd_by_persistence_data = old_idpd
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_identify_task_verbose_pd_queue_head_changed_after_sleep()
    local snap = snapshot_pd_lists()
    local old_sk = skynet.sleep
    local old_idpd = pd_service.identify_pd_by_persistence_data
    pd_service.identify_pd_by_persistence_data = function()
        return false
    end
    local pd_a = {
        key = 'pd_q_a',
        is_valid = function()
            return true
        end,
        locate = function()
            return {
                start = function()
                    return true
                end,
                __close = function() end,
            }
        end,
    }
    local pd_b = {
        key = 'pd_q_b',
        is_valid = function()
            return true
        end,
    }
    skynet.sleep = function()
        pd_service.pd_list = { pd_b, pd_a }
    end
    pd_service.drive_list = {
        {
            Name = 'D_q',
            is_presence = function()
                return true
            end,
            is_being_located = function()
                return true
            end,
        },
    }
    pd_service.pd_list = { pd_a, pd_b }
    lu.assertIsTrue(pd_service:identify_task(PD_MAX_TRY))
    skynet.sleep = old_sk
    pd_service.identify_pd_by_persistence_data = old_idpd
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_identify_task_verbose_pd_invalid_during_drive_scan()
    local snap = snapshot_pd_lists()
    local old_sk = skynet.sleep
    local old_idpd = pd_service.identify_pd_by_persistence_data
    local old_lp = pd_service.locate_pd
    pd_service.identify_pd_by_persistence_data = function()
        return false
    end
    skynet.sleep = function() end
    local pd = {
        key = 'pd_scan_brk',
        valid = true,
        is_valid = function(self)
            return self.valid
        end,
        locate = function()
            return {
                start = function()
                    return true
                end,
                __close = function() end,
            }
        end,
    }
    local n = 0
    pd_service.locate_pd = function(s, p, d)
        local m1, m2 = old_lp(s, p, d)
        n = n + 1
        if n == 1 then
            p.valid = false
        end
        return m1, m2
    end
    local d_bad = {
        Name = 'D_scan_1',
        is_presence = function()
            return false
        end,
        is_being_located = function()
            return false
        end,
    }
    local d_ok = {
        Name = 'D_scan_2',
        is_presence = function()
            return true
        end,
        is_being_located = function()
            return true
        end,
    }
    pd_service.drive_list = { d_bad, d_ok }
    pd_service.pd_list = { pd }
    lu.assertIsTrue(pd_service:identify_task(PD_MAX_TRY))
    pd_service.locate_pd = old_lp
    skynet.sleep = old_sk
    pd_service.identify_pd_by_persistence_data = old_idpd
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_identify_task_verbose_locate_pd_reject_logged()
    local snap = snapshot_pd_lists()
    local old_sk = skynet.sleep
    local old_idpd = pd_service.identify_pd_by_persistence_data
    pd_service.identify_pd_by_persistence_data = function()
        return false
    end
    skynet.sleep = function() end
    local pd = {
        key = 'pd_reject',
        is_valid = function()
            return true
        end,
        locate = function()
            return {
                start = function()
                    return true
                end,
                __close = function() end,
            }
        end,
    }
    pd_service.drive_list = {
        {
            Name = 'D_reject',
            is_presence = function()
                return false
            end,
            is_being_located = function()
                return false
            end,
        },
    }
    pd_service.pd_list = { pd }
    lu.assertIsTrue(pd_service:identify_task(PD_MAX_TRY))
    skynet.sleep = old_sk
    pd_service.identify_pd_by_persistence_data = old_idpd
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_identify_task_verbose_rotate_pd_queue_on_fail()
    local snap = snapshot_pd_lists()
    local old_sk = skynet.sleep
    local old_idpd = pd_service.identify_pd_by_persistence_data
    pd_service.identify_pd_by_persistence_data = function()
        return false
    end
    skynet.sleep = function() end
    local mk_pd = function(key)
        return {
            key = key,
            is_valid = function()
                return true
            end,
            locate = function()
                return {
                    start = function()
                        return true
                    end,
                    __close = function() end,
                }
            end,
        }
    end
    local pda = mk_pd('pd_rot_a')
    local pdb = mk_pd('pd_rot_b')
    pd_service.drive_list = {
        {
            Name = 'D_rot',
            is_presence = function()
                return false
            end,
            is_being_located = function()
                return false
            end,
        },
    }
    pd_service.pd_list = { pda, pdb }
    lu.assertIsTrue(pd_service:identify_task(PD_MAX_TRY))
    lu.assertEquals(pd_service.pd_list[1].key, 'pd_rot_b')
    lu.assertEquals(pd_service.pd_list[2].key, 'pd_rot_a')
    skynet.sleep = old_sk
    pd_service.identify_pd_by_persistence_data = old_idpd
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_identify_task_verbose_cycle_failed_snapshot()
    local snap = snapshot_pd_lists()
    local old_sk = skynet.sleep
    local old_idpd = pd_service.identify_pd_by_persistence_data
    pd_service.identify_pd_by_persistence_data = function()
        return false
    end
    skynet.sleep = function() end
    local pd = {
        key = 'pd_cycle',
        is_valid = function()
            return true
        end,
        locate = function()
            return {
                start = function()
                    return true
                end,
                __close = function() end,
            }
        end,
    }
    pd_service.drive_list = {
        {
            Name = 'D_cycle',
            is_presence = function()
                return false
            end,
            is_being_located = function()
                return false
            end,
        },
    }
    pd_service.pd_list = { pd }
    lu.assertIsTrue(pd_service:identify_task(PD_MAX_TRY))
    skynet.sleep = old_sk
    pd_service.identify_pd_by_persistence_data = old_idpd
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_start_identify_task_stops_loop_when_identify_returns_false()
    local snap = snapshot_pd_lists()
    local old_get_sc = c_storageconfig.get_instance
    local sc = { all_ctrl_loaded = true }
    c_storageconfig.get_instance = function()
        return sc
    end
    local raw_tasks = c_tasks.get_instance
    local loop_body
    c_tasks.get_instance = function()
        return {
            new_task = function()
                return {
                    loop = function(_, fn)
                        loop_body = fn
                        return { set_timeout_ms = function() end }
                    end,
                }
            end,
        }
    end
    local old_identify = pd_service.identify_task
    pd_service.identify_task = function()
        return false
    end
    pd_service.task = nil
    pd_service.add_pd_complete = true
    pd_service.skip = false
    local drive = { Name = 'D_stop', Id = 1, PassThrough = 0, check_link_abnormal = function() end }
    pd_service.drive_list = { drive }
    pd_service.pd_list = {
        {
            key = 'pd_stop',
            is_valid = function()
                return true
            end,
        },
    }
    pd_service:start_identify_task(drive)
    local stopped = false
    loop_body({
        stop = function()
            stopped = true
        end,
    })
    lu.assertIsTrue(stopped)
    lu.assertNil(pd_service.task)
    pd_service.identify_task = old_identify
    c_tasks.get_instance = raw_tasks
    c_storageconfig.get_instance = old_get_sc
    restore_pd_lists(snap)
end

function Test_Pd_Identify_Debug:test_start_identify_task_long_retry_resets_pd_try_count()
    local snap = snapshot_pd_lists()
    local old_get_sc = c_storageconfig.get_instance
    local sc = { all_ctrl_loaded = true }
    c_storageconfig.get_instance = function()
        return sc
    end
    local raw_tasks = c_tasks.get_instance
    local loop_body
    c_tasks.get_instance = function()
        return {
            new_task = function()
                return {
                    loop = function(_, fn)
                        loop_body = fn
                        return { set_timeout_ms = function() end }
                    end,
                }
            end,
        }
    end
    local identify_n = 0
    local old_identify = pd_service.identify_task
    pd_service.identify_task = function()
        identify_n = identify_n + 1
        return true
    end
    pd_service.task = nil
    pd_service.add_pd_complete = true
    pd_service.skip = false
    local drive = { Name = 'D_long', Id = 1, PassThrough = 1, check_link_abnormal = function() end }
    pd_service.drive_list = { drive }
    pd_service.pd_list = {
        {
            key = 'pd_long_a',
            is_valid = function()
                return true
            end,
        },
        {
            key = 'pd_long_b',
            is_valid = function()
                return true
            end,
        },
    }
    pd_service:start_identify_task(drive)
    local fake_task = { stop = function() end }
    for _ = 1, PD_MAX_TRY + 1 do
        loop_body(fake_task)
    end
    lu.assertEquals(identify_n, PD_MAX_TRY + 1)
    pd_service.identify_task = old_identify
    c_tasks.get_instance = raw_tasks
    c_storageconfig.get_instance = old_get_sc
    restore_pd_lists(snap)
end

-- Covers line 666: start_identify_task loop body returns early when skip=true
function Test_Pd_Identify_Debug:test_start_identify_task_loop_returns_when_skip()
    local snap = snapshot_pd_lists()
    local old_get_sc = c_storageconfig.get_instance
    local sc = { all_ctrl_loaded = true }
    c_storageconfig.get_instance = function()
        return sc
    end
    local raw_tasks = c_tasks.get_instance
    local loop_body
    c_tasks.get_instance = function()
        return {
            new_task = function()
                return {
                    loop = function(_, fn)
                        loop_body = fn
                        return { set_timeout_ms = function() end }
                    end,
                }
            end,
        }
    end
    pd_service.task = nil
    pd_service.add_pd_complete = true
    pd_service.skip = true
    local drive = { Name = 'D_skip', Id = 1, PassThrough = 0, check_link_abnormal = function() end }
    pd_service.drive_list = { drive }
    pd_service.pd_list = {
        {
            key = 'pd_skip',
            is_valid = function()
                return true
            end,
        },
    }
    pd_service:start_identify_task(drive)
    local task_stopped = false
    loop_body({
        stop = function()
            task_stopped = true
        end,
    })
    -- skip=true should cause early return before identify_task, so task should NOT be stopped
    lu.assertFalse(task_stopped)
    pd_service.task = nil
    pd_service.skip = false
    c_tasks.get_instance = raw_tasks
    c_storageconfig.get_instance = old_get_sc
    restore_pd_lists(snap)
end

-- Covers lines 678, 679: start_identify_task with PassThrough ~= 1 and try_count > #pd_list
function Test_Pd_Identify_Debug:test_start_identify_task_check_link_abnormal()
    local snap = snapshot_pd_lists()
    local old_get_sc = c_storageconfig.get_instance
    local sc = { all_ctrl_loaded = true }
    c_storageconfig.get_instance = function()
        return sc
    end
    local raw_tasks = c_tasks.get_instance
    local loop_body
    c_tasks.get_instance = function()
        return {
            new_task = function()
                return {
                    loop = function(_, fn)
                        loop_body = fn
                        return { set_timeout_ms = function() end }
                    end,
                }
            end,
        }
    end
    local check_called = false
    local old_identify = pd_service.identify_task
    pd_service.identify_task = function()
        return true
    end
    pd_service.task = nil
    pd_service.add_pd_complete = true
    pd_service.skip = false
    local drive = {
        Name = 'D_link',
        Id = 1,
        PassThrough = 0,
        check_link_abnormal = function()
            check_called = true
        end,
    }
    pd_service.drive_list = { drive }
    pd_service.pd_list = {
        {
            key = 'pd_link_a',
            is_valid = function()
                return true
            end,
        },
    }
    pd_service:start_identify_task(drive)
    local fake_task = { stop = function() end }
    -- First iteration: try_count=0→1, 1 > #pd_list(1) = false
    loop_body(fake_task)
    lu.assertFalse(check_called)
    -- Second iteration: try_count=1→2, 2 > #pd_list(1) = true → check_link_abnormal, try_count=0
    loop_body(fake_task)
    lu.assertTrue(check_called)
    pd_service.identify_task = old_identify
    pd_service.task = nil
    c_tasks.get_instance = raw_tasks
    c_storageconfig.get_instance = old_get_sc
    restore_pd_lists(snap)
end

-- Covers line 656: start_identify_task creates and assigns task
function Test_Pd_Identify_Debug:test_start_identify_task_assigns_task()
    local snap = snapshot_pd_lists()
    local old_get_sc = c_storageconfig.get_instance
    local sc = { all_ctrl_loaded = true }
    c_storageconfig.get_instance = function()
        return sc
    end
    local raw_tasks = c_tasks.get_instance
    c_tasks.get_instance = function()
        return {
            new_task = function()
                return {
                    loop = function(_, fn)
                        return {
                            set_timeout_ms = function(self)
                                return self
                            end,
                        }
                    end,
                }
            end,
        }
    end
    pd_service.task = nil
    pd_service.add_pd_complete = true
    pd_service.skip = false
    local drive = { Name = 'D_assign', Id = 1, PassThrough = 0, check_link_abnormal = function() end }
    pd_service.drive_list = { drive }
    pd_service.pd_list = {
        {
            key = 'pd_assign',
            is_valid = function()
                return true
            end,
        },
    }
    pd_service:start_identify_task(drive)
    lu.assertNotNil(pd_service.task)
    pd_service.task = nil
    c_tasks.get_instance = raw_tasks
    c_storageconfig.get_instance = old_get_sc
    restore_pd_lists(snap)
end

Test_Pd_Object_AutoLocate = {}

function Test_Pd_Object_AutoLocate:test_locate_start_false_when_sml_returns_non_ok()
    local old_op = sml.pd_operation
    local inst = c_tasks.get_instance()
    local old_sleep = inst.sleep_ms
    sml.pd_operation = function()
        return 1
    end
    inst.sleep_ms = function() end
    local pd = { device_id = 9, slot_num = 2, enclosure_id = 3 }
    local obj = pd_object.new(1, pd, 0)
    local loc = obj:locate()
    lu.assertIsFalse(loc:start())
    lu.assertIsFalse(loc.is_locate)
    inst.sleep_ms = old_sleep
    sml.pd_operation = old_op
end

--- STOP_LOCATE 非 RET_OK 时打 notice(pd_object.lua 74-75)
function Test_Pd_Object_AutoLocate:test_stop_locate_notice_when_sml_stop_returns_non_ok()
    local old_op = sml.pd_operation
    local inst = c_tasks.get_instance()
    local old_sleep = inst.sleep_ms
    local old_get_controller = pd_object.get_controller
    inst.sleep_ms = function() end
    pd_object.get_controller = function() end
    local LOCATE_OP = 1
    local STOP_OP = 0
    sml.pd_operation = function(_, _, op, _)
        if op == LOCATE_OP then
            return 0
        end
        if op == STOP_OP then
            return 9
        end
        return 0
    end
    local pd = {
        device_id = 9,
        slot_num = 2,
        enclosure_id = 3,
        controller_id = 4,
        is_valid = function()
            return true
        end,
    }
    local obj = pd_object.new(1, pd, 0)
    local loc = obj:locate()
    lu.assertIsTrue(loc:start())
    loc:__close()
    inst.sleep_ms = old_sleep
    sml.pd_operation = old_op
    pd_object.get_controller = old_get_controller
end

-- Covers line 119: pd_object_operation logs error when sml.pd_operation returns non-OK
function Test_Pd_Object_AutoLocate:test_pd_object_operation_logs_error_on_non_ok()
    local old_op = sml.pd_operation
    sml.pd_operation = function()
        return 1
    end
    local pd = { device_id = 9, slot_num = 2, enclosure_id = 3, controller_id = 4 }
    local obj = pd_object.new(1, pd, 0)
    -- is_valid() returns true by default, sml returns 1 (non-OK) → log:error is called (line 119)
    local retval = obj:pd_object_operation(7, string.pack('B', 1))
    lu.assertEquals(retval, 1)
    sml.pd_operation = old_op
end

-- Covers pd_object_operation when invalid → returns RET_INVALID
function Test_Pd_Object_AutoLocate:test_pd_object_operation_invalid_returns_invalid()
    local pd = { device_id = 9, slot_num = 2, enclosure_id = 3, controller_id = 4 }
    local obj = pd_object.new(1, pd, 0)
    obj:invalid()
    local retval = obj:pd_object_operation(7, '')
    lu.assertEquals(retval, 1)
end

-- Covers set_short_dst
function Test_Pd_Object_AutoLocate:test_set_short_dst_success()
    local old_op = sml.pd_operation
    sml.pd_operation = function()
        return 0
    end
    local pd = { device_id = 9, slot_num = 2, enclosure_id = 3, controller_id = 4 }
    local obj = pd_object.new(1, pd, 0)
    local retval = obj:set_short_dst(1)
    lu.assertEquals(retval, 0)
    sml.pd_operation = old_op
end
function Test_Pd_Object_AutoLocate:test_locate_start_set_led_when_directly_manage()
    local old_op = sml.pd_operation
    local inst = c_tasks.get_instance()
    local old_sleep = inst.sleep_ms
    local old_get_controller = pd_object.get_controller
    local old_drive = drive_collection.get_instance
    inst.sleep_ms = function() end

    local led_calls = {}
    drive_collection.get_instance = function()
        return {
            get_drive_by_drive_id = function(_, id)
                return {
                    set_locate_led = function(_, val)
                        table.insert(led_calls, { drive_id = id, led = val })
                    end,
                }
            end,
        }
    end

    pd_object.get_controller = function()
        return {
            is_directly_manage_physical_drive = function()
                return true
            end,
        }
    end

    sml.pd_operation = function()
        return 0
    end

    local pd = {
        device_id = 9,
        slot_num = 2,
        enclosure_id = 3,
        controller_id = 4,
        is_valid = function()
            return true
        end,
    }
    local obj = pd_object.new(1, pd, 0)
    local loc = obj:locate()
    lu.assertIsTrue(loc:start())

    lu.assertEquals(#led_calls, 1)
    lu.assertEquals(led_calls[1].drive_id, 209)
    lu.assertEquals(led_calls[1].led, 1)

    -- 必须关闭定位对象:否则 loc 在后续用例中被GC回收时,__gc会触发stop()
    -- 复用后续用例的打桩调用set_locate_led,导致下一个用例的led_calls被污染
    loc:__close()

    inst.sleep_ms = old_sleep
    sml.pd_operation = old_op
    pd_object.get_controller = old_get_controller
    drive_collection.get_instance = old_drive
end

function Test_Pd_Object_AutoLocate:test_get_controller_returns_controller_by_id()
    local old_cc = package.loaded['controller.controller_collection']
    package.loaded['controller.controller_collection'] = nil
    local controller_collection = require('controller.controller_collection')
    local old_get_instance = controller_collection.get_instance

    local expected_ctrl = { Id = 1, TypeId = 200 }
    controller_collection.get_instance = function()
        return {
            get_by_controller_id = function(_, ctrl_id)
                lu.assertEquals(ctrl_id, 1)
                return expected_ctrl
            end,
        }
    end

    local pd = { device_id = 9, slot_num = 2, enclosure_id = 3 }
    local obj = pd_object.new(1, pd, 0)
    local ctrl = obj:get_controller()
    lu.assertEquals(ctrl, expected_ctrl)

    controller_collection.get_instance = old_get_instance
    package.loaded['controller.controller_collection'] = old_cc
end

function Test_Pd_Object_AutoLocate:test_locate_stop_set_led_when_directly_manage()
    local old_op = sml.pd_operation
    local inst = c_tasks.get_instance()
    local old_sleep = inst.sleep_ms
    local old_get_controller = pd_object.get_controller
    local old_drive = drive_collection.get_instance
    inst.sleep_ms = function() end

    local led_calls = {}
    drive_collection.get_instance = function()
        return {
            get_drive_by_drive_id = function(_, id)
                return {
                    set_locate_led = function(_, val)
                        table.insert(led_calls, { drive_id = id, led = val })
                    end,
                }
            end,
        }
    end

    pd_object.get_controller = function()
        return {
            is_directly_manage_physical_drive = function()
                return true
            end,
        }
    end

    local LOCATE_OP = 1
    local STOP_OP = 0
    sml.pd_operation = function(_, _, op, _)
        if op == LOCATE_OP then
            return 0
        end
        if op == STOP_OP then
            return 0
        end
        return 0
    end

    local pd = {
        device_id = 9,
        slot_num = 2,
        enclosure_id = 3,
        controller_id = 4,
        is_valid = function()
            return true
        end,
    }
    local obj = pd_object.new(1, pd, 0)
    local loc = obj:locate()
    lu.assertIsTrue(loc:start())
    loc:__close()

    lu.assertEquals(#led_calls, 2)
    lu.assertEquals(led_calls[1].led, 1)
    lu.assertEquals(led_calls[2].led, 0)

    inst.sleep_ms = old_sleep
    sml.pd_operation = old_op
    pd_object.get_controller = old_get_controller
    drive_collection.get_instance = old_drive
end