-- 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.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.


local skynet = require 'skynet'
local log = require 'mc.logging'
local mdb = require 'mc.mdb'

require 'fructrl.json_types.ChassisFruCtrl'
require 'fructrl.json_types.ChassisPowerOnLock'
require 'fructrl.json_types.SystemLockDown'
require 'fructrl.json_types.SystemsFruCtrl'

local RUN_UNTIL_MAX_RETRY<const> = 20
local RUN_UNTIL_ONE_TICK_DELAY<const> = 50

local cases = {}

local CHASSIS_FRUCTRL_INTF<const> = 'bmc.kepler.Chassis.FruCtrl'
local SYS_POWERON_LOCK_INTF<const> = 'bmc.kepler.Systems.PowerOnLock'
local CHASSIS_POWERON_LOCK_INTF<const> = 'bmc.kepler.Chassis.PowerOnLock'
local SYS_FRUCTRL_INTF<const> = 'bmc.kepler.Systems.FruCtrl'

local function get_mdb_object(bus, path, intf)
    local mobj
    for _ = 1, 5 do
        mobj = mdb.get_object(bus, path, intf)
        if mobj then
            break
        end
        skynet.sleep(100)
    end
    if not mobj then
        log:error('get mdb object [%s:%s] timeout.', path, intf)
    end
    return mobj
end

local function get_host_power_state(system_id)
    local test_data_dir = skynet.getenv('TEST_DATA_DIR')
    local host_power_state_file = test_data_dir .. 'power_states/' .. 'host_' .. system_id .. '_power_state'

    local fd = io.open(host_power_state_file, "r")
    if not fd then
        return nil
    end
    local content = fd:read("*all")
    fd:close()

    -- 去除前后的空字符
    return content:gsub("^%s*(.-)%s*$", "%1")
end

local function run_until(cb, max_timeout_tick)
    max_timeout_tick = max_timeout_tick == nil and RUN_UNTIL_MAX_RETRY or max_timeout_tick
    for i = 1, max_timeout_tick do
        if cb() then
            return true
        end
        skynet.sleep(RUN_UNTIL_ONE_TICK_DELAY)
    end
    return false
end

local function check_host_status(host_seq_list, prev_status, expect_status, step)
    local flag = true
    for idx = 1, #host_seq_list do
        if idx <= step then
            flag = flag and (get_host_power_state(host_seq_list[idx]) == expect_status)
        else
            if prev_status then
                flag = flag and (get_host_power_state(host_seq_list[idx]) == prev_status)
            else
                flag = flag and (get_host_power_state(host_seq_list[idx]) ~= expect_status)
            end
        end
    end
    return flag
end

local function assert_status_change_in_sequence(host_seq_list, valid_step, prev_status, expect_status, skip_first)
    if host_seq_list == nil or next(host_seq_list) == nil then
        log:warning("host_seq_list empty, please input host status change sequence")
        return
    end
    assert(valid_step >= 0 and valid_step <= #host_seq_list)
    for step = 0, valid_step do
        if step == 0 and skip_first ~= nil and skip_first == true then
            goto continue
        end
        assert(run_until(function()
            return check_host_status(host_seq_list, prev_status, expect_status, step)
        end))
        ::continue::
    end
end

local function test_chassis_fructrl_prop_val(chassis_fructrl_obj)
    assert(chassis_fructrl_obj.PowerState == "ON", "chassis power state" .. chassis_fructrl_obj.PowerState)
end

-- 测试接口边界值
local function test_boundary_value(chassis_fructrl_obj)
    -- 用例1：PowerCtrlType参数异常
    local ok, err
    ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "ON", "Unknown")
    assert(not ok and err.name == 'PropertyValueNotInList')
    ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "ForceOn", "Unknown")
    assert(not ok and err.name == 'PropertyValueNotInList')
    -- 用例2：cause参数异常
    ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "On", "Unknownn")
    assert(not ok and err.name == 'PropertyValueNotInList')
end

-- 测试RPC接口权限
local function test_rpc_privilege(bus, chassis_fructrl_obj)
    local privilege = {
        ReadOnly = 2 ^ 0,
        DiagnoseMgmt = 2 ^ 1,
        SecurityMgmt = 2 ^ 2,
        BasicSetting = 2 ^ 3,
        UserMgmt = 2 ^ 4,
        PowerMgmt = 2 ^ 5,
        VMMMgmt = 2 ^ 6,
        KVMMgmt = 2 ^ 7,
        ConfigureSelf = 2 ^ 8
    }
    local ctx = require('mc.context').new()
    ctx.Auth = "1"
    ctx.Privilege = tostring(privilege.ReadOnly | privilege.DiagnoseMgmt | privilege.SecurityMgmt |
        privilege.BasicSetting | privilege.UserMgmt | privilege.VMMMgmt | privilege.KVMMgmt | privilege.ConfigureSelf)
    -- SetPowerOnLock接口权限测试
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(ctx, "GracefulShutdown", "Unknown")
    assert(not ok and err.name == 'InsufficientPrivilege')

    ctx.Privilege = tostring(privilege.PowerMgmt | privilege.ReadOnly)
    ok, _ = chassis_fructrl_obj.pcall:PowerCtrl(ctx, "GracefulShutdown", "Unknown")
    assert(ok)

    -- SetPowerOnLock接口权限测试
    local chassis_poweron_lock_obj = get_mdb_object(bus, '/bmc/kepler/Chassis/1/FruCtrl', CHASSIS_POWERON_LOCK_INTF)
    ctx.Privilege = tostring(privilege.ReadOnly | privilege.DiagnoseMgmt | privilege.SecurityMgmt |
        privilege.BasicSetting | privilege.UserMgmt | privilege.VMMMgmt | privilege.KVMMgmt | privilege.ConfigureSelf)
    ok, err = chassis_poweron_lock_obj.pcall:SetPowerOnLock(ctx, true, 10, "firmware_mgmt", "upgrade")
    assert(not ok and err.name == 'InsufficientPrivilege')

    -- SetPowerOnStrategyExceptions接口权限测试
    ctx.Privilege = tostring(privilege.ReadOnly | privilege.DiagnoseMgmt | privilege.SecurityMgmt |
        privilege.BasicSetting | privilege.UserMgmt | privilege.VMMMgmt | privilege.KVMMgmt | privilege.ConfigureSelf)
    ok, err = chassis_fructrl_obj.pcall:SetPowerOnStrategyExceptions(ctx, "01234567891", "Yes", "Once", 3)
    assert(not ok and err.name == 'InsufficientPrivilege')
end

local function test_chassis_poweron(chassis_fructrl_obj)
    -- 用例1：整机上电接口，host依次上电
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "On", "Unknown")
    assert(ok, err)
    assert_status_change_in_sequence({1, 2, 3, 4, 5}, 4, nil, "On", true)
end

-- 用例2：整机下电接口，host一起下电
local function test_chassis_fraceful_shutdown(chassis_fructrl_obj)
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "GracefulShutdown", "Unknown")
    assert(ok, err)
    skynet.sleep(50)
    assert((get_host_power_state(1) == "GracefulShutdown") and (get_host_power_state(2) == "GracefulShutdown") and
        (get_host_power_state(3) == "GracefulShutdown") and (get_host_power_state(4) == "GracefulShutdown"))
end

-- 用例3：整机强制下电接口，host一起下电
local function test_chassis_forceoff(chassis_fructrl_obj)
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "ForceOff", "Unknown")
    assert(ok, err)
    skynet.sleep(50)
    assert((get_host_power_state(1) == "ForceOff") and (get_host_power_state(2) == "ForceOff") and
        (get_host_power_state(3) == "ForceOff") and (get_host_power_state(4) == "ForceOff"))
end

-- 用例4：整机Nmi接口，host一起调用Nmi
local function test_chassis_nmi(chassis_fructrl_obj)
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "Nmi", "Unknown")
    assert(ok, err)
    skynet.sleep(50)
    assert((get_host_power_state(1) == "Nmi") and (get_host_power_state(2) == "Nmi") and
        (get_host_power_state(3) == "Nmi") and (get_host_power_state(4) == "Nmi"))
end

local function test_chassis_powercycle(chassis_fructrl_obj)
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "PowerCycle", "Unknown")
    assert(ok, err)
    assert_status_change_in_sequence({1, 2, 3, 4}, 4, "GracefulShutdown", "On")
end

-- 先调整机下电接口，再调整机上电接口，预期最后上电
local function test_chassis_poweroff_then_poweron(chassis_fructrl_obj)
    log:notice('================ test chassis power off then power on start ================')
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "GracefulShutdown", "Unknown")
    assert(ok, err)
    skynet.sleep(50)
    assert((get_host_power_state(1) == "GracefulShutdown") and (get_host_power_state(2) == "GracefulShutdown") and
        (get_host_power_state(3) == "GracefulShutdown") and (get_host_power_state(4) == "GracefulShutdown"))

    ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "On", "Unknown")
    assert(ok, err)
    assert_status_change_in_sequence({1, 2, 3, 4}, 4, "GracefulShutdown", "On", true)
    -- 恢复环境
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "ForceOff", "Unknown")
    assert(ok, err)
    skynet.sleep(50)
    assert((get_host_power_state(1) == "ForceOff") and (get_host_power_state(2) == "ForceOff") and
        (get_host_power_state(3) == "ForceOff") and (get_host_power_state(4) == "ForceOff"))
    log:notice('================ test chassis power off then power on finish ================')
end

-- 先调整机上电接口，再调整机下电接口，预期最后下电
local function test_chassis_poweron_then_poweroff(chassis_fructrl_obj)
    -- 先上电再强制下电
    log:notice('================ test chassis power on then poweroff start ================')
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "On", "Unknown")
    assert(ok, err)
    assert_status_change_in_sequence({1, 2, 3, 4}, 2, nil, "On", true)

    ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "ForceOff", "Unknown")
    assert(ok, err)
    assert(run_until(function()
        return (get_host_power_state(1) == "ForceOff") and (get_host_power_state(2) == "ForceOff") and
        (get_host_power_state(3) == "ForceOff") and (get_host_power_state(4) == "ForceOff")
    end))
    log:notice('================ test chassis power on then power off finish ================')
end

-- 先调整机上电接口，再调整机Power cycle接口，预期最后上电
local function test_chassis_poweroff_then_powercycle(chassis_fructrl_obj)
    log:notice('================ test chassis power off then power cycle start ================')
    local ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "On", "Unknown")
    assert(ok, err)
    assert_status_change_in_sequence({1, 2, 3, 4}, 2, nil, "On", true)

    ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "ForcePowerCycle", "Unknown")
    assert(ok, err)
    assert_status_change_in_sequence({1, 2, 3, 4}, 4, "ForceOff", "On")
    log:notice('================ test chassis power off then power cycle finish ================')
end

-- 整机接口与单host接口交叉调用
local function test_system_restart_then_chassis_restart(bus, chassis_fructrl_obj)
    -- 先调host2的复位接口
    local fru_path = '/bmc/kepler/Systems/2/FruCtrl'
    local err
    local ok, sys_fructrl_obj = pcall(mdb.get_sub_objects, bus, fru_path, SYS_FRUCTRL_INTF)
    assert(ok, sys_fructrl_obj)
    for _, obj in pairs(sys_fructrl_obj) do
        ok, err = obj.pcall:PowerCtrl(require('mc.context').new(), "ForceRestart", "Unknown")
        assert(ok, err)
    end
    -- 再调host3的复位接口
    fru_path = '/bmc/kepler/Systems/3/FruCtrl'
    ok, sys_fructrl_obj = pcall(mdb.get_sub_objects, bus, fru_path, SYS_FRUCTRL_INTF)
    assert(ok, sys_fructrl_obj)
    for _, obj in pairs(sys_fructrl_obj) do
        ok, err = obj.pcall:PowerCtrl(require('mc.context').new(), "ForceRestart", "Unknown")
        assert(ok, err)
    end
    -- 只有处于非整机下电,才调整机复位接口
    if chassis_fructrl_obj.PowerState == 'ON' then
        ok, err = chassis_fructrl_obj.pcall:PowerCtrl(require('mc.context').new(), "ForceRestart", "Unknown")
        assert(ok, err)
    end

    assert_status_change_in_sequence({2, 3, 1, 4}, 3, nil, "ForceRestart", true)
    -- 此时再次执行 host2 复位操作
    assert(run_until(function()
        return (get_host_power_state(1) == "ForceRestart") and (get_host_power_state(2) == "ForceRestart") and
        (get_host_power_state(3) == "ForceRestart") and (get_host_power_state(4) ~= "ForceRestart")
    end))
    assert(run_until(function()
        return (get_host_power_state(1) == "ForceRestart") and (get_host_power_state(2) == "ForceRestart") and
        (get_host_power_state(3) == "ForceRestart") and (get_host_power_state(4) == "ForceRestart")
    end))
end

-- 测试整机上电锁
local function test_chassis_poweron_lock(bus)
    local chassis_poweron_lock_obj = get_mdb_object(bus, '/bmc/kepler/Chassis/1/FruCtrl', CHASSIS_POWERON_LOCK_INTF)

    local ok, err = chassis_poweron_lock_obj.pcall:SetPowerOnLock(require('mc.context').new(), true, 10,
        "firmware_mgmt", "upgrade")
    assert(ok, err)
    skynet.sleep(100)

    local sys_poweron_lock_objs, fru_path
    for system_id = 1, 4 do
        fru_path = '/bmc/kepler/Systems/' .. system_id .. '/FruCtrl'
        ok, sys_poweron_lock_objs = pcall(mdb.get_sub_objects, bus, fru_path, SYS_POWERON_LOCK_INTF)
        assert(ok, sys_poweron_lock_objs)
        for _, obj in pairs(sys_poweron_lock_objs) do
            assert(obj.PwrOnLocked and obj.Reasons[1] == 'upgrade', "res: ".. tostring(obj.PwrOnLocked) .. " " ..
                obj.Reasons[1])
        end
    end

    skynet.sleep(800)
    for system_id = 1, 4 do
        fru_path = '/bmc/kepler/Systems/' .. system_id .. '/FruCtrl'
        ok, sys_poweron_lock_objs = pcall(mdb.get_sub_objects, bus, fru_path, SYS_POWERON_LOCK_INTF)
        assert(ok, sys_poweron_lock_objs)
        for _, obj in pairs(sys_poweron_lock_objs) do
            assert(obj.PwrOnLocked and obj.Reasons[1] == 'upgrade', "res: ".. tostring(obj.PwrOnLocked) .. " " ..
                obj.Reasons[1])
        end
    end

    skynet.sleep(150)
    for system_id = 1, 4 do
        fru_path = '/bmc/kepler/Systems/' .. system_id .. '/FruCtrl'
        ok, sys_poweron_lock_objs = pcall(mdb.get_sub_objects, bus, fru_path, SYS_POWERON_LOCK_INTF)
        assert(ok, sys_poweron_lock_objs)
        for _, obj in pairs(sys_poweron_lock_objs) do
            assert(not obj.PwrOnLocked and not obj.Reasons[1], "res: ".. tostring(obj.PwrOnLocked) .. " " ..
                tostring(obj.Reasons[1]))
        end
    end
end

-- 测试整机AC功能
local function test_chassis_accycle(bus)
    local chassis_chassis_accycle_obj = get_mdb_object(bus, '/bmc/kepler/Chassis/1/FruCtrl', CHASSIS_FRUCTRL_INTF)
    local ok, err = chassis_chassis_accycle_obj.pcall:PowerCtrl(require('mc.context').new(), 'ACCycle', 'Unknown')
    assert(ok, err)
end

local function test_systems_powercycle(bus)
    local chassis_obj = get_mdb_object(bus, '/bmc/kepler/Chassis/1/FruCtrl', CHASSIS_FRUCTRL_INTF)
    local ok, err = chassis_obj.pcall:PowerCtrl(require('mc.context').new(), 'On', 'Unknown')
    assert(ok, err)
    skynet.sleep(500)
    local fru_path
    local system_objs
    for system_id = 1, 4 do
        fru_path = '/bmc/kepler/Systems/' .. system_id .. '/FruCtrl'
        ok, system_objs = pcall(mdb.get_sub_objects, bus, fru_path, SYS_FRUCTRL_INTF)
        assert(ok, system_objs)
        for _, obj in pairs(system_objs) do
            ok, err = obj.pcall:PowerCtrl(require('mc.context').new(), "PowerCycle", "Unknown")
            assert(ok, err)
        end
    end
end

function cases.test_entry(bus)
    log:notice('================ test chassis power ctrl start ================')
    local ok, err = pcall(function ()
        local chassis_fructrl_obj = get_mdb_object(bus, '/bmc/kepler/Chassis/1/FruCtrl', CHASSIS_FRUCTRL_INTF)
        -- 自发现对象初始值验证
        test_chassis_fructrl_prop_val(chassis_fructrl_obj)
        -- 接口边界值校验
        test_boundary_value(chassis_fructrl_obj)
        -- 接口权限测试
        test_rpc_privilege(bus, chassis_fructrl_obj)
        -- 整机上下电接口测试
        test_chassis_poweron(chassis_fructrl_obj)
        test_chassis_fraceful_shutdown(chassis_fructrl_obj)
        test_chassis_forceoff(chassis_fructrl_obj)
        test_chassis_nmi(chassis_fructrl_obj)
        test_chassis_powercycle(chassis_fructrl_obj)
        -- 先调整机下电接口，再调整机上电接口，预期最后上电
        test_chassis_poweroff_then_poweron(chassis_fructrl_obj)
        -- 先调整机上电接口，再调整机下电接口，预期最后下电
        test_chassis_poweron_then_poweroff(chassis_fructrl_obj)
        -- 先调整机上电接口，再调整机Power cycle接口，预期最后上电
        test_chassis_poweroff_then_powercycle(chassis_fructrl_obj)
        -- 整机接口与单host接口交叉调用
        test_system_restart_then_chassis_restart(bus, chassis_fructrl_obj)

        test_chassis_poweron_lock(bus)
        test_chassis_accycle(bus)
        -- 先调整机上电接口,在调system下的powercycle接口,预期最后上电
        test_systems_powercycle(bus)
    -- 恢复环境
    end)
    assert(ok, err)
    log:notice('================ test chassis power ctrl completed ================')
end

return cases