#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#

"""
Tests for the sysinv agent pci utilities.
"""

import mock
try:
    from contextlib import nested  # Python 2
except ImportError:
    from contextlib import ExitStack
    from contextlib import contextmanager

    @contextmanager
    def nested(*contexts):
        """
        Reimplementation of nested in python 3.
        """
        with ExitStack() as stack:
            yield tuple(stack.enter_context(cm) for cm in contexts)

from sysinv.agent.pci import PCIOperator
from sysinv.agent.pci import PCI
from sysinv.agent.manager import AgentManager
from sysinv.tests import base
from sysinv.common import fpga_constants
import tsconfig.tsconfig as tsc

FAKE_LSPCI_OUTPUT = {
    '82:00.0':
    """Slot:   82:00.0
       Class:  Ethernet controller
       Vendor: Intel Corporation
       Device: 82599ES 10-Gigabit SFI/SFP+ Network Connection
       SVendor:        Intel Corporation
       SDevice:        Ethernet Server Adapter X520-2
       PhySlot:        803
       Rev:    01
       Driver: ixgbe
       Module: ixgbe
       NUMANode:""",
    '82:10.0':
    """Slot:   82:10.0
       Class:  Ethernet controller
       Vendor: Intel Corporation
       Device: 82599 Ethernet Controller Virtual Function
       SVendor:        Intel Corporation
       SDevice:        Device 000c
       Rev:    01
       Driver: vfio-pci
       Module: ixgbevf
       NUMANode:       1""",
    'b4:00.0':
    """Slot:   b4:00.0
       Class:  Processing accelerators
       Vendor: Intel Corporation
       Device: Device 0d8f
       SVendor:        Intel Corporation
       SDevice:        Device 0001
       Rev:    01
       Driver: igb_uio
       NUMANode:       1""",
    'b4:00.1':
    """Slot:   b4:00.1
       Class:  Processing accelerators
       Vendor: Intel Corporation
       Device: Device 0d90
       SVendor:        Intel Corporation
       SDevice:        Device e001
       Rev:    01
       Driver: igb_uio
       NUMANode:       1"""
}

FAKE_DEVICE_ATTRIBUTES = [{
    "name": "fake_device",
    "pci_address": "b4:00.0",
    "pclass_id": "120000",
    "pvendor_id": "8086",
    "pdevice_id": "0d8f",
    "numa_node": 0,
    "sriov_totalvfs": 8,
    "sriov_numvfs": 1,
    "sriov_vfs_pci_address": "b4:00.1",
    "sriov_vf_driver": "igb_uio",
    "sriov_vf_pdevice_id": "0d90",
    "driver": "igb_uio",
    "enabled": True,
}]

FAKE_PORT_ATTRIBUTES = [{
    "name": "fake_port",
    "numa_node": 0,
    "sriov_totalvfs": 8,
    "sriov_numvfs": 1,
    "sriov_vfs_pci_address": "82:10.0",
    "sriov_vf_driver": "ixgbevf",
    "sriov_vf_pdevice_id": "000c",
    "driver": "ixgbe",
    "pci_address": "82:00.0",
    "mac": "3c:fd:fe:b5:72:fd",
    "mtu": 1500,
    "speed": 1000,
    "link_mode": 0,
    "dev_id": 1,
    "dpdksupport": True
}]

FAKE_PORTS = [PCI(
    '82:10.0', 'Ethernet controller [0200]', 'Intel Corporation [8086]', 'Device [1518]',
    '-r04', 'Intel Corporation [8086]', 'Device [0000]')]

FAKE_DEVICES = [PCI(
    'b4:00.0', 'Processing accelerators', 'Intel Corporation [8086]', 'Device [0d8f]',
    '', 'Intel Corporation [8086]', 'Device [0000]')]


class TestPciOperator(base.TestCase):

    def setUp(self):
        super(TestPciOperator, self).setUp()
        self.pci_operator = PCIOperator()

    def tearDown(self):
        super(TestPciOperator, self).tearDown()

    def mock_get_lspci_output_by_addr(addr):  # pylint: disable=no-self-argument
        return FAKE_LSPCI_OUTPUT[addr]

    def mock_subprocess_popen(self):
        # Mock the subprocess.Popen call
        mock_stdout = mock.Mock()
        mock_stdout.stdout.read.return_value = b'''
        0000:f7:00.0 "Co-processor" "Intel Corporation" "Device 4942" "-r40" "Intel Corporation" "Device 0000",
        0000:f3:00.0 "Co-processor" "Intel Corporation" "Device 4042" "-r40" "Intel Corporation" "Device 0000"'''
        p = mock.patch('eventlet.green.subprocess.Popen')
        self.mock_subprocess = p.start()
        self.mock_subprocess.return_value = mock_stdout
        self.addCleanup(p.stop)

    @mock.patch.object(PCIOperator, 'get_lspci_output_by_addr',
                       side_effect=mock_get_lspci_output_by_addr)
    def test_get_pci_sriov_vf_driver_name(self, get_lspci_output_by_addr):
        pfaddr = '82:00.0'
        vfaddrs = ['82:10.0']
        result = self.pci_operator.get_pci_sriov_vf_driver_name(pfaddr, vfaddrs)
        assert result == "vfio-pci"

    @mock.patch.object(PCIOperator, 'get_lspci_output_by_addr',
                       side_effect=mock_get_lspci_output_by_addr)
    def test_get_pci_sriov_vf_module_name(self, get_lspci_output_by_addr):
        pfaddr = '82:00.0'
        vfaddrs = ['82:10.0']
        result = self.pci_operator.get_pci_sriov_vf_module_name(pfaddr, vfaddrs)
        assert result == "ixgbevf"

    @mock.patch.object(PCIOperator, 'get_lspci_output_by_addr',
                       side_effect=mock_get_lspci_output_by_addr)
    def test_get_pci_sriov_vf_module_name_none(self, get_lspci_output_by_addr):
        pfaddr = 'b4:00.0'
        vfaddrs = ['b4:00.1']
        result = self.pci_operator.get_pci_sriov_vf_module_name(pfaddr, vfaddrs)
        assert result is None

    def test_get_supported_device_plugin_gpu(self):
        # testing gpu device plugin only
        self.fake_pci_device_list = [
                {
                    'name': 'pci_0000_00_01_1',
                    'pclass_id': '01018a',
                    'pclass': 'IDE interface',
                    'pvendor_id': '8086',
                    'pdevice_id': '1e20',
                    'sriov_vfs_pci_address': '',
                    'driver': 'ata_piix'
                },
                {
                    'name': 'pci_0000_00_02_0',
                    'pclass_id': '030000',
                    'pclass': 'VGA compatible controller',
                    'pvendor_id': '8086',
                    'pdevice_id': '1e20',
                    'sriov_vfs_pci_address': '',
                    'driver': 'i915'
                },
                {
                    'name': 'pci_0000_00_05_0',
                    'pclass_id': '040100',
                    'pclass': 'Multimedia audio controller',
                    'pvendor_id': '8086',
                    'pdevice_id': '1e20',
                    'sriov_vfs_pci_address': '',
                    'driver': None
                },
                {
                    'name': 'pci_0000_00_0d_0',
                    'pclass_id': '010601',
                    'pclass': 'VGA compatible controller',
                    'pvendor_id': '8086',
                    'pdevice_id': '1e20',
                    'sriov_vfs_pci_address': '',
                    'driver': 'ahci'
                },
                {
                    'name': 'pci_0000_00_01_2',
                    'pclass_id': '01018b',
                    'pclass': 'VGA compatible controller',
                    'pvendor_id': '8086',
                    'pdevice_id': '1e20',
                    'sriov_vfs_pci_address': '',
                    'driver': None
                }]
        # Mock the subprocess.Popen call
        mock_stdout = mock.Mock()
        mock_stdout.stdout.read.return_value = None
        p = mock.patch('eventlet.green.subprocess.Popen')
        self.mock_subprocess = p.start()
        self.mock_subprocess.return_value = mock_stdout
        self.addCleanup(p.stop)
        # Verify the expected label_key and label_value with the actual output
        expected_label = ['intelgpu']
        actual_label = self.pci_operator.get_supported_device_plugins(self.fake_pci_device_list)
        self.assertEqual(expected_label, actual_label)

    def test_get_supported_device_plugin_qat(self):
        # testing qat device plugin only
        self.mock_subprocess_popen()
        self.fake_pci_device_list = [
                {
                    'name': 'pci_0000_00_01_1',
                    'pclass_id': '01018a',
                    'pclass': 'IDE interface',
                    'pvendor_id': '8086',
                    'pdevice_id': '2710',
                    'sriov_vfs_pci_address': '',
                    'driver': 'ata_piix'
                },
                {
                    'name': 'pci_0000_00_05_0',
                    'pclass_id': '040100',
                    'pclass': 'Multimedia audio controller',
                    'pvendor_id': '8086',
                    'pdevice_id': '37c9',
                    'sriov_vfs_pci_address': '',
                    'driver': None
                },
                {
                    "name": "pci_0000_f7_00_0",
                    "pciaddr": "0000:f7:00.0",
                    "pclass_id": "0b4000",
                    "pvendor_id": "8086",
                    'pdevice_id': '4942',
                    "pclass": "Co-processor",
                    "pvendor": "Intel Corporation",
                    "pdevice": "Device 4942",
                    "prevision": "-r40",
                    "psvendor": "Intel Corporation",
                    "psdevice": "Device 0000",
                    "numa_node": "0",
                    "sriov_totalvfs": "16",
                    "sriov_numvfs": "16",
                    "sriov_vfs_pci_address": '''0000:f7:00.1,0000:f7:00.2,0000:f7:00.3,0000:f7:00.4,0000:f7:00.5,
                    0000:f7:00.6,0000:f7:00.7,0000:f7:01.0,0000:f7:01.1,0000:f7:01.2,0000:f7:01.3,0000:f7:01.4,
                    0000:f7:01.5,0000:f7:01.6,0000:f7:01.7,0000:f7:02.0''',
                    "driver": "4xxx"
                }]
        expected_label = ['intelqat']
        actual_label = self.pci_operator.get_supported_device_plugins(self.fake_pci_device_list)
        self.assertEqual(expected_label, actual_label)

    def test_get_supported_device_plugin_qat_and_gpu(self):
        # testing both gpu and qat device plugin
        self.mock_subprocess_popen()
        self.fake_pci_device_list = [
                {
                    'name': 'pci_0000_00_01_1',
                    'pclass_id': '01018a',
                    'pclass': 'IDE interface',
                    'pvendor_id': '8086',
                    'pdevice_id': '1e20',
                    'sriov_vfs_pci_address': '',
                    'driver': 'ata_piix'
                },
                {
                    'name': 'pci_0000_00_02_0',
                    'pclass_id': '030000',
                    'pclass': 'VGA compatible controller',
                    'pvendor_id': '8086',
                    'pdevice_id': '2710',
                    'sriov_vfs_pci_address': '',
                    'driver': 'i915'
                },
                {
                    "name": "pci_0000_f7_00_0",
                    "pciaddr": "0000:f7:00.0",
                    "pclass_id": "0b4000",
                    "pvendor_id": "8086",
                    'pdevice_id': '4942',
                    "pclass": "Co-processor",
                    "pvendor": "Intel Corporation",
                    "pdevice": "Device 4942",
                    "prevision": "-r40",
                    "psvendor": "Intel Corporation",
                    "psdevice": "Device 0000",
                    "numa_node": "0",
                    "sriov_totalvfs": "16",
                    "sriov_numvfs": "16",
                    "sriov_vfs_pci_address": '''0000:f7:00.1,0000:f7:00.2,0000:f7:00.3,0000:f7:00.4,0000:f7:00.5,
                    0000:f7:00.6,0000:f7:00.7,0000:f7:01.0,0000:f7:01.1,0000:f7:01.2,0000:f7:01.3,0000:f7:01.4,
                    0000:f7:01.5,0000:f7:01.6,0000:f7:01.7,0000:f7:02.0''',
                    "driver": "4xxx"
                }]
        expected_label = ['intelgpu', 'intelqat']
        actual_label = self.pci_operator.get_supported_device_plugins(self.fake_pci_device_list)
        self.assertEqual(expected_label, actual_label)


class TestAgentOperator(base.TestCase):

    def setUp(self):
        super(TestAgentOperator, self).setUp()
        self.agent_manager = AgentManager('test-host', 'test-topic')

    def tearDown(self):
        super(TestAgentOperator, self).tearDown()

    def _get_ports_inventory(self):
        with nested(mock.patch.object(PCIOperator, 'pci_get_net_attrs'),
                    mock.patch.object(PCIOperator, 'pci_get_device_attrs'),
                    mock.patch.object(PCIOperator, 'inics_get'),
                    mock.patch.object(PCIOperator, 'pci_devices_get'),
                    mock.patch.object(AgentManager, '_acquire_network_config_lock'),
                    mock.patch.object(AgentManager, '_release_network_config_lock')) as (
                        mock_net_attrs, mock_device_attrs, mock_nics, mock_devices,
                        aquire_lock, release_lock):

            def fake_get_net_attrs(a):
                return FAKE_PORT_ATTRIBUTES

            def fake_get_device_attrs(a):
                return FAKE_DEVICE_ATTRIBUTES

            def fake_get_nics():
                return FAKE_PORTS

            def fake_get_devices():
                return FAKE_DEVICES

            mock_net_attrs.side_effect = fake_get_net_attrs
            mock_device_attrs.side_effect = fake_get_device_attrs
            mock_nics.side_effect = fake_get_nics
            mock_devices.side_effect = fake_get_devices

            ports, devices, macs = self.agent_manager._get_ports_inventory()
            return ports, devices, macs

    @mock.patch('os.path.exists')
    def test_get_pci_inventory_before_worker_initial_config_complete(self, mock_exists):
        def file_exists_side_effect(filename):
            # Neither the initial nor volatile worker config complete flags are set
            return False
        mock_exists.side_effect = file_exists_side_effect

        ports, devices, macs = self._get_ports_inventory()
        assert len(ports) == 1
        assert len(devices) == 1
        assert len(macs) == 1

    @mock.patch('os.path.exists')
    def test_get_pci_inventory_before_worker_config_complete(self, mock_exists):
        def file_exists_side_effect(filename):
            if filename == tsc.INITIAL_WORKER_CONFIG_COMPLETE:
                # Only the initial worker config complete flag is set
                return True
            else:
                return False
        mock_exists.side_effect = file_exists_side_effect

        ports, devices, macs = self._get_ports_inventory()
        assert len(ports) == 0
        assert len(devices) == 0
        assert len(macs) == 0

    @mock.patch('os.path.exists')
    def test_get_pci_inventory_after_worker_config_complete(self, mock_exists):
        def file_exists_side_effect(filename):
            if filename in [tsc.INITIAL_WORKER_CONFIG_COMPLETE,
                            tsc.VOLATILE_WORKER_CONFIG_COMPLETE]:
                # Both of the initial and volatile worker config complete flags are set
                return True
            else:
                return False
        mock_exists.side_effect = file_exists_side_effect

        ports, devices, macs = self._get_ports_inventory()
        for dev in devices:
            assert dev['fpga_n3000_reset'] is False
        assert len(ports) == 1
        assert len(devices) == 1
        assert len(macs) == 1

    @mock.patch('os.path.exists')
    def test_get_pci_inventory_n3000_reset_flag(self, mock_exists):
        def file_exists_side_effect(filename):
            if filename in [tsc.INITIAL_WORKER_CONFIG_COMPLETE,
                            tsc.VOLATILE_WORKER_CONFIG_COMPLETE,
                            fpga_constants.N3000_RESET_FLAG]:
                return True
            else:
                return False
        mock_exists.side_effect = file_exists_side_effect

        ports, devices, macs = self._get_ports_inventory()
        for dev in devices:
            assert dev['fpga_n3000_reset'] is True
        assert len(ports) == 1
        assert len(devices) == 1
        assert len(macs) == 1
