################################################################################
# BSD LICENSE
#
# Copyright(c) 2019-2023 Intel Corporation. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in
#     the documentation and/or other materials provided with the
#     distribution.
#   * Neither the name of Intel Corporation nor the names of its
#     contributors may be used to endorse or promote products derived
#     from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
################################################################################

"""
Unit tests for appqos.power module
"""

import mock
import pytest

from appqos import power
from appqos import power_common
from appqos import sstbf
from appqos.config import Config

try:
    import pwr

    HAS_PWR = True
except ImportError:
    HAS_PWR = False

class TestRestPowerProfiles:


    @pytest.mark.skipif(not HAS_PWR, reason="pwr package not installed")
    def test_is_sstcp_enabled(self):
        class SYS:
            def __init__(self, enabled):
                self.epp_enabled = enabled
                self.sst_bf_enabled = enabled

        with mock.patch("pwr.get_system", return_value=SYS(True)) as mock_get_system:
            assert power.is_sstcp_enabled()
            mock_get_system.assert_called_once()

        with mock.patch("pwr.get_system", return_value=SYS(False)) as mock_get_system:
            assert not power.is_sstcp_enabled()
            mock_get_system.assert_called_once()

        with mock.patch("pwr.get_system", return_value=None) as mock_get_system:
            assert not power.is_sstcp_enabled()
            mock_get_system.assert_called_once()

        with mock.patch("pwr.get_system", side_effect = IOError('Test')) as mock_get_system:
            assert not power.is_sstcp_enabled()
            mock_get_system.assert_called_once()


    @pytest.mark.skipif(not HAS_PWR, reason="pwr package not installed")
    def test__get_pwr_cpus(self):
        class CPU:
            def __init__(self, core_list):
                self.core_list=core_list

        CPUS_LIST = [CPU([]), CPU([])]
        with mock.patch("pwr.get_cpus", return_value=CPUS_LIST) as mock_get_cpus:
            assert power_common.get_pwr_cpus() == CPUS_LIST
            mock_get_cpus.assert_called_once()

        with mock.patch("pwr.get_cpus", return_value=[]) as mock_get_cpus:
            assert not power_common.get_pwr_cpus()
            mock_get_cpus.assert_called_once()

        with mock.patch("pwr.get_cpus", return_value=None) as mock_get_cpus:
            assert not power_common.get_pwr_cpus()
            mock_get_cpus.assert_called_once()

        with mock.patch("pwr.get_cpus", side_effect = IOError('Test')) as mock_get_cpus:
            assert not power_common.get_pwr_cpus()
            mock_get_cpus.assert_called_once()

        with mock.patch("pwr.get_cpus", side_effect = ValueError('Test')) as mock_get_cpus:
            assert not power_common.get_pwr_cpus()
            mock_get_cpus.assert_called_once()


    @pytest.mark.skipif(not HAS_PWR, reason="pwr package not installed")
    def test__get_pwr_cores(self):
        class CORE:
            def __init__(self, id):
                self.core_id=id

        CORES_LIST = [CORE(0), CORE(1)]

        with mock.patch("pwr.get_cores", return_value=CORES_LIST) as mock_get_cores:
            assert power_common.get_pwr_cores() == CORES_LIST
            mock_get_cores.assert_called_once()

        with mock.patch("pwr.get_cores", return_value=[]) as mock_get_cores:
            assert not power_common.get_pwr_cores()
            mock_get_cores.assert_called_once()

        with mock.patch("pwr.get_cores", return_value=None) as mock_get_cores:
            assert not power_common.get_pwr_cores()
            mock_get_cores.assert_called_once()

        with mock.patch("pwr.get_cores", side_effect = IOError('Test')) as mock_get_cores:
            assert not power_common.get_pwr_cores()
            mock_get_cores.assert_called_once()

        with mock.patch("pwr.get_cores", side_effect = ValueError('Test')) as mock_get_cores:
            assert not power_common.get_pwr_cores()
            mock_get_cores.assert_called_once()


    def test__set_freqs_epp(self):
        class CORE:
            def __init__(self, id):
                self.core_id=id
                self.commit = mock.MagicMock()

        CORES_LIST = [CORE(0), CORE(1), CORE(2), CORE(3), CORE(4)]

        assert -1 == power._set_freqs_epp(cores=None, min_freq=1000)

        assert -1 == power._set_freqs_epp(cores=[], min_freq=1000)

        assert -1 == power._set_freqs_epp(cores=[1,2,3], min_freq=None, max_freq=None, epp=None)

        for ret_val in [[], None]:
            with mock.patch("appqos.power_common.get_pwr_cores", return_value=ret_val) as mock_get_cores:
                assert -1 == power._set_freqs_epp(cores=[1,2,3], min_freq=1000, max_freq=2300, epp="performance")
                mock_get_cores.assert_called_once()

        with mock.patch("appqos.power_common.get_pwr_cores", return_value=CORES_LIST) as mock_get_cores:
            assert 0 == power._set_freqs_epp(cores=[1,2,3], min_freq=1000, max_freq=2300, epp="performance")
            mock_get_cores.assert_called_once()

            for core in CORES_LIST:
                if core.core_id in [1, 2, 3]:
                    assert core.min_freq == 1000
                    assert core.max_freq == 2300
                    assert core.epp == "performance"
                    core.commit.assert_called_once()
                else:
                    assert not hasattr(core, 'min_freq')
                    assert not hasattr(core, 'max_freq')
                    assert not hasattr(core, 'epp')
                    core.commit.assert_not_called()


    def test_reset(self):
        class CORE:
            def __init__(self, id):
                self.core_id=id
                self.commit = mock.MagicMock()

        CORES_LIST = [CORE(0), CORE(1), CORE(2), CORE(3), CORE(4)]

        for val in [[], None]:
            assert -1 == power.reset(val)

        with mock.patch("appqos.power_common.get_pwr_cores", return_value=CORES_LIST) as mock_get_cores:
            assert 0 == power.reset([0, 1, 2])
            mock_get_cores.assert_called_once()

            for core in CORES_LIST:
                if core.core_id in [0, 1, 2]:
                    core.commit.assert_called_once_with("default")
                else:
                    core.commit.assert_not_called()


    def test__get_lowest_freq(self):
        class CPU:
            def __init__(self, freq):
                self.lowest_freq = freq

        for ret_val in [[], None]:
            with mock.patch("appqos.power_common.get_pwr_cpus", return_value = ret_val) as mock_get_cpus:
                assert None == power_common.get_pwr_lowest_freq()
                mock_get_cpus.assert_called_once()

        for freq in [1000, 1500, 2000]:
            with mock.patch("appqos.power_common.get_pwr_cpus", return_value=[CPU(freq)]) as mock_get_cpus:
                assert freq == power_common.get_pwr_lowest_freq()
                mock_get_cpus.assert_called_once()


    def test__get_base_freq(self):
        class CPU:
            def __init__(self, freq):
                self.base_freq = freq

        for ret_val in [[], None]:
            with mock.patch("appqos.power_common.get_pwr_cpus", return_value = ret_val) as mock_get_cpus:
                assert None == power_common.get_pwr_base_freq()
                mock_get_cpus.assert_called_once()

        for freq in [2000, 2500, 3000]:
            with mock.patch("appqos.power_common.get_pwr_cpus", return_value=[CPU(freq)]) as mock_get_cpus:
                assert freq == power_common.get_pwr_base_freq()
                mock_get_cpus.assert_called_once()


    def test__get_highest_freq(self):
        class CPU:
            def __init__(self, freq):
                self.highest_freq = freq

        for ret_val in [[], None]:
            with mock.patch("appqos.power_common.get_pwr_cpus", return_value = ret_val) as mock_get_cpus:
                assert None == power_common.get_pwr_highest_freq()
                mock_get_cpus.assert_called_once()

        for freq in [3000, 3500, 4000]:
            with mock.patch("appqos.power_common.get_pwr_cpus", return_value=[CPU(freq)]) as mock_get_cpus:
                assert freq == power_common.get_pwr_highest_freq()
                mock_get_cpus.assert_called_once()


    def test__is_min_freq_valid(self):

        for ret_val in [[], None]:
            with mock.patch("appqos.power_common.get_pwr_lowest_freq", return_value = ret_val) as mock_get:
                assert None == power._is_min_freq_valid(1000)
                mock_get.assert_called_once()

        for freq in [500, 1000, 1499]:
            with mock.patch("appqos.power_common.get_pwr_lowest_freq", return_value = 1500) as mock_get:
                assert False == power._is_min_freq_valid(freq)
                mock_get.assert_called_once()

        for freq in [1000, 1500, 2000]:
            with mock.patch("appqos.power_common.get_pwr_lowest_freq", return_value = 1000) as mock_get:
                assert True == power._is_min_freq_valid(freq)
                mock_get.assert_called_once()


    def test__is_max_freq_valid(self):

        for ret_val in [[], None]:
            with mock.patch("appqos.power_common.get_pwr_highest_freq", return_value = ret_val) as mock_get:
                assert None == power._is_max_freq_valid(1000)
                mock_get.assert_called_once()

        for freq in [3501, 3750, 4000]:
            with mock.patch("appqos.power_common.get_pwr_highest_freq", return_value = 3500) as mock_get:
                assert False == power._is_max_freq_valid(freq)
                mock_get.assert_called_once()

        for freq in [2000, 3000, 3500]:
            with mock.patch("appqos.power_common.get_pwr_highest_freq", return_value = 3500) as mock_get:
                assert True == power._is_max_freq_valid(freq)
                mock_get.assert_called_once()


    def test__is_epp_valid(self):

        for epp in [None, "", 1000, "TEST", "inValid_EPP", []]:
            assert False == power._is_epp_valid(epp)

        for epp in power.VALID_EPP + [power.DEFAULT_EPP]:
            assert True == power._is_epp_valid(epp)


    def test__do_admission_control_check(self):

        for sstbf in [True, False]:
            result = not sstbf

            with mock.patch('appqos.sstbf.is_sstbf_configured', return_value = sstbf):
                assert result == power._do_admission_control_check()


    def test_validate_power_profiles(self):

        data = {
            "power_profiles": [
                {
                    "id": 0,
                    "min_freq": 1500,
                    "max_freq": 2500,
                    "epp": "performance",
                    "name": "default"
                },
                {
                    "id": 0,
                    "min_freq": 1000,
                    "max_freq": 1000,
                    "epp": "power",
                    "name": "low_priority"
                }
            ]
        }

        with mock.patch('appqos.power._is_max_freq_valid', return_value = True) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid', return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power._admission_control_check', return_value = True) as mock_admission_control_check:

            with pytest.raises(ValueError, match="Power Profile 0, multiple profiles with same id."):
                power.validate_power_profiles(data, True)
            mock_admission_control_check.assert_not_called()

        # fix profile ID issue
        data['power_profiles'][-1]['id'] = 1

        with mock.patch('appqos.power._is_max_freq_valid', return_value = True) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid', return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power._admission_control_check', return_value = True) as mock__admission_control_check:

            power.validate_power_profiles(data, True)

            mock_is_max.assert_any_call(2500)
            mock_is_max.assert_any_call(1000)

            mock_is_min.assert_any_call(1500)
            mock_is_min.assert_any_call(1000)

            mock_is_epp.assert_any_call("performance")
            mock_is_epp.assert_any_call("power")

            mock__admission_control_check.assert_called_once()


        with mock.patch('appqos.power._is_max_freq_valid', return_value = True) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid', return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = False),\
            mock.patch('appqos.power._admission_control_check') as mock__admission_control_check:

            power.validate_power_profiles(data, True)
            mock__admission_control_check.assert_not_called()


        with mock.patch('appqos.power._is_max_freq_valid', return_value = True) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid', return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power._admission_control_check') as mock__admission_control_check:

            power.validate_power_profiles(data, False)
            mock__admission_control_check.assert_not_called()


        sys = mock.MagicMock()
        sys.request_config = mock.MagicMock(return_value=True)
        sys.refresh_all = mock.MagicMock()

        cores = []
        for id in range(0,3):
            core = mock.MagicMock()
            core.core_id = id
            cores.append(core)
        data.update({
                    "pools": [
                    {
                        "id": 0,
                        "cores": [0,1,2,3],
                        "power_profile": 0
                    },
                    {
                        "id": 1,
                        "cores": [4,5,6,7]
                    }]
        })
        with mock.patch('appqos.power._is_max_freq_valid', return_value = True) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid', return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power_common.get_pwr_cores', return_value=cores) as mock_get_cores,\
            mock.patch('appqos.power_common.get_pwr_sys', return_value=sys) as mock_get_sys:

            power.validate_power_profiles(data, True)
            sys.request_config.assert_called_once()
            sys.refresh_all.assert_called_once()

            for core in cores:
                assert hasattr(core, "min_freq")
                assert hasattr(core, "max_freq")
                assert hasattr(core, "epp")


        sys.request_config = mock.MagicMock(return_value=False)
        sys.refresh_all.reset_mock()
        with mock.patch('appqos.power._is_max_freq_valid', return_value = True) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid', return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power_common.get_pwr_cores', return_value=cores) as mock_get_cores,\
            mock.patch('appqos.power_common.get_pwr_sys', return_value=sys) as mock_get_sys:

            with pytest.raises(power.AdmissionControlError, match="Power Profiles configuration would cause CPU to be oversubscribed."):
                power.validate_power_profiles(data, True)
            sys.request_config.assert_called_once()
            sys.refresh_all.assert_called_once()


        with mock.patch('appqos.power._is_max_freq_valid',  return_value = False ) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid',  return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power._admission_control_check', return_value = True) as mock__admission_control_check:

            with pytest.raises(ValueError, match="Power Profile 0, Invalid max. freq 2500."):
                power.validate_power_profiles(data, True)
            mock__admission_control_check.assert_not_called()


        with mock.patch('appqos.power._is_max_freq_valid',  return_value = True ) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid',  return_value = False) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power._admission_control_check', return_value = True) as mock__admission_control_check:

            with pytest.raises(ValueError, match="Power Profile 0, Invalid min. freq 1500."):
                power.validate_power_profiles(data, True)
            mock__admission_control_check.assert_not_called()


        with mock.patch('appqos.power._is_max_freq_valid',  return_value = True ) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid',  return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = False) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power._admission_control_check', return_value = True) as mock__admission_control_check:

            with pytest.raises(ValueError, match="Power Profile 0, Invalid EPP value performance."):
                power.validate_power_profiles(data, True)
            mock__admission_control_check.assert_not_called()


        # set invalid min. freq, higher than max. freq
        data['power_profiles'][-1]['min_freq'] = data['power_profiles'][-1]['max_freq'] + 1

        with mock.patch('appqos.power._is_max_freq_valid',  return_value = True) as mock_is_max,\
            mock.patch('appqos.power._is_min_freq_valid',  return_value = True) as mock_is_min,\
            mock.patch('appqos.power._is_epp_valid', return_value = True) as mock_is_epp,\
            mock.patch('appqos.power._do_admission_control_check', return_value = True),\
            mock.patch('appqos.power._admission_control_check', return_value = True) as mock__admission_control_check:

            with pytest.raises(ValueError, match="Power Profile 1, Invalid freqs! min. freq is higher than max. freq."):
                power.validate_power_profiles(data, True)
            mock__admission_control_check.assert_not_called()


    def test_configure_power(self):

        pool_ids = [0, 1]
        profile_ids = [5, 6]
        pool_to_cores = {0: [2,3], 1: [4, 5]}
        pool_to_profiles = {0: 5, 1: 6}
        profiles = {5: {"min_freq": 1500, "max_freq": 2500, "epp": "performance", "id": 5},
            6 : {"min_freq": 1000, "max_freq": 1200, "epp": "power", "id": 6}}

        def get_pool_attr(cls, attr, pool_id):
            if attr == 'id':
                if pool_id is None:
                    return pool_ids
                else:
                    return None
            elif attr == 'cores':
                return pool_to_cores[pool_id]
            elif attr == 'power_profile':
                if pool_id is None:
                    return profile_ids
                else:
                    return pool_to_profiles[pool_id]

            return None

        def get_power_profile(cls, id):
            return profiles[id]

        # ERROR POWER: No Pools configured...
        power.PREV_PROFILES.clear()
        with mock.patch('appqos.config.Config.get_pool_attr', return_value=None) as mock_get_pool_attr,\
            mock.patch('appqos.config.Config.get_power_profile') as mock_get_power_profile,\
            mock.patch('appqos.power.reset') as mock_reset,\
            mock.patch('appqos.power._set_freqs_epp') as mock_set_freqs_epp:

            assert -1 == power.configure_power(Config({}))
            mock_get_pool_attr.assert_called_once_with('id', None)
            mock_get_power_profile.assert_not_called()
            mock_reset.assert_not_called()
            mock_set_freqs_epp.assert_not_called()

        # ERROR POWER: Profile 5 does not exist!
        power.PREV_PROFILES.clear()
        with mock.patch('appqos.config.Config.get_pool_attr', new=get_pool_attr),\
            mock.patch('appqos.config.Config.get_power_profile', return_value=None) as mock_get_power_profile,\
            mock.patch('appqos.power.reset') as mock_reset,\
            mock.patch('appqos.power._set_freqs_epp') as mock_set_freqs_epp:

            assert -1 == power.configure_power(Config({}))
            mock_get_power_profile.assert_called_once_with(5)
            mock_reset.assert_not_called()
            mock_set_freqs_epp.assert_not_called()

        # All OK!
        power.PREV_PROFILES.clear()
        with mock.patch('appqos.config.Config.get_pool_attr', new=get_pool_attr),\
             mock.patch('appqos.config.Config.get_power_profile', new=get_power_profile),\
             mock.patch('appqos.power.reset') as mock_reset,\
             mock.patch('appqos.power._set_freqs_epp') as mock_set_freqs_epp:

            assert 0 == power.configure_power(Config({}))
            mock_reset.assert_not_called()
            mock_set_freqs_epp.assert_any_call(pool_to_cores[0], profiles[5]["min_freq"], profiles[5]["max_freq"], profiles[5]["epp"])
            mock_set_freqs_epp.assert_any_call(pool_to_cores[1], profiles[6]["min_freq"], profiles[6]["max_freq"], profiles[6]["epp"])

        # POWER: Skipping Pool 0, no cores assigned
        power.PREV_PROFILES.clear()
        pool_to_cores[0] = []
        with mock.patch('appqos.config.Config.get_pool_attr', new=get_pool_attr),\
            mock.patch('appqos.config.Config.get_power_profile', new=get_power_profile),\
            mock.patch('appqos.power.reset') as mock_reset,\
            mock.patch('appqos.power._set_freqs_epp') as mock_set_freqs_epp:

            assert 0 == power.configure_power(Config({}))
            mock_reset.assert_not_called()
            mock_set_freqs_epp.assert_called_once_with(pool_to_cores[1], profiles[6]["min_freq"], profiles[6]["max_freq"], profiles[6]["epp"])

        # POWER: Pool 1, no power profile assigned. Resetting to defaults.
        power.PREV_PROFILES.clear()
        pool_to_profiles[1] = None
        with mock.patch('appqos.config.Config.get_pool_attr', new=get_pool_attr),\
            mock.patch('appqos.config.Config.get_power_profile', new=get_power_profile),\
            mock.patch('appqos.power.reset') as mock_reset,\
            mock.patch('appqos.power._set_freqs_epp') as mock_set_freqs_epp:

            assert 0 == power.configure_power(Config({}))
            mock_reset.assert_called_once_with(pool_to_cores[1])
            mock_set_freqs_epp.assert_not_called()
