-- 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 signal = require 'mc.signal'
local hook_orm_tasks = require 'hook_orm_tasks'

-- bus 需提供 match，否则 init -> start -> monitor_bma -> register_bma_resource_signal 会报错
local function make_stub_bus(overrides)
    local bus = {
        match = function(_, sig, cb)
            return { free = function() end }
        end,
        call = function() return { value = function() return 0 end } end,
        pcall = function() return nil, {} end
    }
    if overrides then
        for k, v in pairs(overrides) do bus[k] = v end
    end
    return bus
end

local stub_queue_fn = function(fn) fn() end

local sms_status_cb = nil
local stub_client = {
    OnSmsStatusPropertiesChanged = function(_, cb)
        sms_status_cb = cb
    end
}

local stub_mdb_get_object_ret = nil
local stub_mdb_get_object_err = nil

TestBmaMdbMgmt = {}

function TestBmaMdbMgmt:setUp()
    package.loaded['bma.bma_mdb_mgmt'] = nil
    package.loaded['skynet.queue'] = nil
    package.loaded['storage.client'] = nil
    package.loaded['mc.mdb.mdb_service'] = nil
    sms_status_cb = nil
    stub_mdb_get_object_ret = nil
    stub_mdb_get_object_err = nil

    -- 使用 hook_orm_tasks 打桩，保证 task 有 on_task_exit，避免 bma_mdb_mgmt:93 报错
    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
        return inst
    end

    package.preload['skynet.queue'] = function()
        return function() return stub_queue_fn end
    end
    package.preload['storage.client'] = function()
        return stub_client
    end
    package.preload['mc.mdb.mdb_service'] = function()
        return {
            get_object = function(bus, path, opts)
                if stub_mdb_get_object_err then
                    error(stub_mdb_get_object_err)
                end
                return stub_mdb_get_object_ret
            end
        }
    end
end

function TestBmaMdbMgmt: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['bma.bma_mdb_mgmt'] = nil
    package.loaded['mc.orm.tasks'] = nil
    package.loaded['skynet.queue'] = nil
    package.loaded['storage.client'] = nil
    package.loaded['mc.mdb.mdb_service'] = nil
    package.preload['skynet.queue'] = nil
    package.preload['storage.client'] = nil
    package.preload['mc.mdb.mdb_service'] = nil
end

function TestBmaMdbMgmt:test_ctor()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bma = { bus = make_stub_bus(), on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    lu.assertNotNil(mgmt.on_add)
    lu.assertNotNil(mgmt.on_update)
    lu.assertNotNil(mgmt.on_delete)
    lu.assertNotNil(mgmt.on_reset)
    lu.assertNotNil(mgmt.queue)
    lu.assertEquals(mgmt.bma, bma)
    lu.assertEquals(mgmt.bus, bma.bus)
end

function TestBmaMdbMgmt:test_start_without_bus()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bma = { bus = nil, on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    mgmt:start()
    lu.assertNil(mgmt.add_slot)
end

function TestBmaMdbMgmt:test_add_reload_path_idempotent()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bma = { bus = make_stub_bus(), on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    mgmt.reload_task = nil
    mgmt.reload_queue = {}
    mgmt.tasks = { new_task = function() return { loop = function() end, on_task_exit = { on = function() end }, stop = function() end } end }
    mgmt:add_reload_path('/path/a')
    lu.assertEquals(mgmt.reload_queue['/path/a'], true)
    lu.assertEquals(#mgmt.reload_queue, 1)
    mgmt:add_reload_path('/path/a')
    lu.assertEquals(#mgmt.reload_queue, 1)
end

function TestBmaMdbMgmt:test_load_bma_resource_emits_on_add()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bma = { bus = make_stub_bus(), on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local emitted_path, emitted_data
    mgmt.on_add:on(function(path, data)
        emitted_path = path
        emitted_data = data
    end)
    local path_interfaces = {
        ['bmc.kepler.sms'] = { ['@odata.id'] = '/Storage/1', Name = 'Test' }
    }
    mgmt:load_bma_resource('/bmc/kepler/Systems/1/Sms/1/ComputerSystem/Systems/1/Storage/1', path_interfaces)
    lu.assertEquals(emitted_path, '/bmc/kepler/Systems/1/Sms/1/ComputerSystem/Systems/1/Storage/1')
    lu.assertNotNil(emitted_data)
    lu.assertEquals(emitted_data.Name, 'Test')
end

function TestBmaMdbMgmt:test_load_bma_resource_empty_interfaces_no_emit()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bma = { bus = make_stub_bus(), on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local emit_count = 0
    mgmt.on_add:on(function() emit_count = emit_count + 1 end)
    mgmt:load_bma_resource('/path', {})
    lu.assertEquals(emit_count, 0)
end

function TestBmaMdbMgmt:test_update_bma_resource_emits_on_update()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bma = { bus = make_stub_bus(), on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local emitted_path, emitted_data
    mgmt.on_update:on(function(path, data)
        emitted_path = path
        emitted_data = data
    end)
    mgmt:update_bma_resource('/Storage/1', 'bmc.kepler.sms', { Status = 'OK' })
    lu.assertNotNil(emitted_path)
    lu.assertEquals(emitted_data.Status, 'OK')
end

function TestBmaMdbMgmt:test_delete_resource_emits_on_delete()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bma = { bus = make_stub_bus(), on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local emitted_path
    mgmt.on_delete:on(function(path) emitted_path = path end)
    mgmt:delete_resource('/Storage/1')
    lu.assertNotNil(emitted_path)
end

function TestBmaMdbMgmt:test_register_and_unregister_bma_resource_signal()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local match_count = 0
    local bus = make_stub_bus({
        match = function(_, sig, cb)
            match_count = match_count + 1
            return { free = function() end }
        end
    })
    local bma = { bus = bus, on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    mgmt:register_bma_resource_signal()
    -- 使用 hook_orm_tasks 时 load 任务退出也会注册一次，故 match 可能为 3 或 6
    lu.assertTrue(match_count >= 3, string.format('match_count should be >= 3, got %d', match_count))
    lu.assertNotNil(mgmt.add_slot)
    lu.assertNotNil(mgmt.del_slot)
    lu.assertNotNil(mgmt.prop_changed_slot)
    mgmt:unregister_bma_resource_signal()
    lu.assertNil(mgmt.add_slot)
    lu.assertNil(mgmt.del_slot)
    lu.assertNil(mgmt.prop_changed_slot)
end

function TestBmaMdbMgmt:test_monitor_bma_state_1_unregisters_and_emits_reset()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bus = make_stub_bus()
    local bma = { bus = bus, on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local unreg_called, reset_emitted
    mgmt.unregister_bma_resource_signal = function() unreg_called = true end
    mgmt.on_reset:on(function() reset_emitted = true end)
    mgmt:register_bma_resource_signal()
    mgmt:monitor_bma()
    lu.assertNotNil(sms_status_cb)
    if sms_status_cb then
        sms_status_cb({ State = { value = function() return 1 end } }, nil, nil)
    end
    lu.assertTrue(unreg_called)
    lu.assertTrue(reset_emitted)
end

function TestBmaMdbMgmt:test_get_bma_resource_from_mdb_mgmt_success()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    stub_mdb_get_object_ret = {
        Object = {
            ['bmc.kepler.host_agent'] = { 'bmc.kepler.sms' }
        }
    }
    local bus = make_stub_bus({
        call = function(_, app, path, iface, method, sig, name)
            return { ['@odata.id'] = path, Name = 'R1' }
        end
    })
    local bma = { bus = bus, on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local data = mgmt:get_bma_resource_from_mdb_mgmt('/path')
    lu.assertNotNil(data)
    lu.assertNotNil(data['bmc.kepler.sms'])
    lu.assertEquals(data['bmc.kepler.sms'].Name, 'R1')
end

function TestBmaMdbMgmt:test_get_bma_resource_from_mdb_mgmt_no_ifaces_returns_nil()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    stub_mdb_get_object_ret = { Object = { ['bmc.kepler.host_agent'] = nil } }
    local bma = { bus = make_stub_bus(), on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local data = mgmt:get_bma_resource_from_mdb_mgmt('/path')
    lu.assertNil(data)
end

function TestBmaMdbMgmt:test_reload_bma_resource_to_json_success()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local emitted_path, emitted_data
    local bus = make_stub_bus({
        pcall = function(_, app, path, iface, method, sig, args)
            return nil, '{"Name":"FromJson"}'
        end
    })
    local bma = { bus = bus, on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    mgmt.on_add:on(function(path, data)
        emitted_path = path
        emitted_data = data
    end)
    mgmt:reload_bma_resource('/Storage/1')
    lu.assertNotNil(emitted_data)
    lu.assertEquals(emitted_data.Name, 'FromJson')
end

function TestBmaMdbMgmt:test_reload_bma_resource_unknown_method_fallback_to_mdb()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    stub_mdb_get_object_ret = {
        Object = { ['bmc.kepler.host_agent'] = { 'bmc.kepler.sms' } }
    }
    local call_count = 0
    local bus = make_stub_bus({
        pcall = function(_, app, path, iface, method, sig, args)
            call_count = call_count + 1
            if call_count == 1 then
                return { name = 'org.freedesktop.DBus.Error.UnknownMethod' }, nil
            end
            return nil, nil
        end,
        call = function(_, app, path, iface, method, sig, name)
            return { ['@odata.id'] = path, Id = 'R1' }
        end
    })
    local bma = { bus = bus, on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local emitted = false
    mgmt.on_add:on(function() emitted = true end)
    mgmt:reload_bma_resource('/Storage/1')
    lu.assertTrue(emitted)
end

function TestBmaMdbMgmt:test_load_all_bma_resource_service_unknown_exits()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local bus = make_stub_bus({
        pcall = function()
            return { name = 'org.freedesktop.DBus.Error.ServiceUnknown' }, nil
        end
    })
    local bma = { bus = bus, on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local add_count = 0
    mgmt.on_add:on(function() add_count = add_count + 1 end)
    mgmt:load_all_bma_resource('/bmc/kepler/Systems/1/Sms/1/ComputerSystem/Systems/1/Storage', 1)
    lu.assertEquals(add_count, 0)
end

function TestBmaMdbMgmt:test_load_all_bma_resource_success_loads_and_stops()
    local c_bma_mdb_mgmt = require 'bma.bma_mdb_mgmt'
    local pcall_count = 0
    local bus = make_stub_bus({
        pcall = function(_, app, path)
            pcall_count = pcall_count + 1
            if pcall_count == 1 then
                return nil, {
                    ['/bmc/kepler/Systems/1/Sms/1/ComputerSystem/Systems/1/Storage/1'] = {
                        ['bmc.kepler.sms'] = { ['@odata.id'] = '/Storage/1', Id = '1' }
                    }
                }
            end
            return nil, {}
        end
    })
    local bma = { bus = bus, on_need_resource = signal.new() }
    local mgmt = c_bma_mdb_mgmt.new(bma)
    local add_count = 0
    mgmt.on_add:on(function() add_count = add_count + 1 end)
    mgmt:load_all_bma_resource('/bmc/kepler/Systems/1/Sms/1/ComputerSystem/Systems/1/Storage', 2)
    lu.assertTrue(add_count > 0)
end
