from __future__ import print_function
# coding=utf-8
"""
Test reading and writing configuration files as well as configuration objects.

Copyright (C) 2014 Radek Novacek <rnovacek@redhat.com>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""

import os
import shutil
from tempfile import mkdtemp
from binascii import hexlify, unhexlify
from mock import patch
import logging

from base import TestBase, unittest

from virtwho.config import DestinationToSourceMapper, parse_list, Satellite6DestinationInfo, init_config, \
    VW_GLOBAL, VW_ENV_CLI_SECTION_NAME
import virtwho.config

from virtwho.password import Password, InvalidKeyFile

default_config_values = {
    "name": "test",
    "type": "esx",
    "server": "1.2.3.4",
    "username": "admin",
    "password": "password",
    "owner": "root",
    "rhsm_username": "admin",
    "rhsm_password": "password",
    "rhsm_hostname": "host",
    "rhsm_port": "1234",
    "rhsm_prefix": "prefix",
    "rhsm_proxy_hostname": "proxy host",
    "rhsm_proxy_port": "4321",
    "rhsm_proxy_user": "proxyuser",
    "rhsm_proxy_password": "proxypass",
    "rhsm_insecure": "1"
}


def combine_dicts(*args):
    """
    A utility method to combine all dictionaries passed into one
    @param args: One or more dicts
    @type args: dict

    @return: dict with the combined values from all args. NOTE: The value
    for any key in more than one dict will be the value of the last dict
    in the arg list that has that key.
    @rtype: dict
    """
    result = {}
    for arg in args:
        result.update(arg)
    return result


def append_number_to_all(in_dict, number):
    result = {}
    for key, value in in_dict.items():
        result[key] = value + str(number)
    return result


class TestReadingConfigs(TestBase):
    source_options_1 = {
        "name": "test1",
        "type": "esx",  # The following values are sensitive to the type involved
        "server": "https://1.2.3.4",  # for example, "http://" is needed here
        "username": "admin",
        "password": "password",
    }
    source_options_2 = {
        "name": "test2",
        "type": "esx",
        "server": "https://1.2.3.5",
        "username": "admin",
        "password": "password",
    }
    dest_options = {
        "owner": "root",
        "rhsm_username": "rhsm_admin",
        "rhsm_password": "rhsm_password",
        "rhsm_hostname": "host",
        "rhsm_port": "1234",
        "rhsm_prefix": "prefix",
        "rhsm_proxy_hostname": "proxyhost",
        "rhsm_proxy_port": "4321",
        "rhsm_proxy_user": "proxyuser",
        "rhsm_proxy_password": "proxypass",
        "rhsm_insecure": ""
    }
    dest_options_1 = append_number_to_all(dest_options, 1)
    dest_options_2 = append_number_to_all(dest_options, 2)

    def setUp(self):
        self.config_dir = mkdtemp()
        self.custom_config_dir = mkdtemp()
        self.general_config_file_dir = mkdtemp()
        self.addCleanup(shutil.rmtree, self.config_dir, self.custom_config_dir, self.general_config_file_dir)
        self.logger = logging.getLogger("virtwho.main")

    def tearDown(self):
        virtwho.config.VW_GENERAL_CONF_PATH = '/etc/virt-who.conf'

    @staticmethod
    def dict_to_ini(in_dict):
        """
        A utility method that formats the given dict as a section of an ini

        @param in_dict: The dictionary containing the keys and values to be
        made ini-like. The section name returned by this method will be the
        value of the "name" key in this dict.
        @type in_dict: dict

        @return: A string formatted like an ini file section
        @rtype: str
        """
        header = "[%s]\n" % in_dict.get("name", "test")
        body = "\n".join([
            "%s=%s" % (key, val)
            for key, val
            in in_dict.items()
            if key != "name"
        ])
        return header + body + "\n"

    def test_empty_config(self):
        config = init_config({}, self.config_dir)
        self.assertCountEqual(list(config.keys()), [VW_GLOBAL, VW_ENV_CLI_SECTION_NAME])

    def assert_config_equals_default(self, config):
        self.assertEqual(config.name, "test")
        self.assertEqual(config['type'], "esx")
        self.assertEqual(config['server'], "https://1.2.3.4")
        self.assertEqual(config['username'], "admin")
        self.assertEqual(config['password'], "password")
        self.assertEqual(config['owner'], "root")
        self.assertEqual(config['rhsm_username'], 'admin')
        self.assertEqual(config['rhsm_password'], 'password')
        self.assertEqual(config['rhsm_hostname'], 'host')
        self.assertEqual(config['rhsm_port'], '1234')
        self.assertEqual(config['rhsm_prefix'], 'prefix')
        self.assertEqual(config['rhsm_proxy_hostname'], 'proxy host')
        self.assertEqual(config['rhsm_proxy_port'], '4321')
        self.assertEqual(config['rhsm_proxy_user'], 'proxyuser')
        self.assertEqual(config['rhsm_proxy_password'], 'proxypass')
        self.assertEqual(config['rhsm_insecure'], '1')
        self.assertEqual(config['simplified_vim'], True)

    def assert_config_contains_all(self, config, options):
        for key in options:
            if key == 'name':
                config_value = getattr(config, key)
            else:
                config_value = config.get(key, None)
            self.assertEqual(config_value, options[key])

    def test_basic_config(self):
        with open(os.path.join(self.config_dir, "test.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(default_config_values))
        config = init_config({}, config_dir=self.config_dir)
        self.assertCountEqual(list(config.keys()), ['test', 'global'])
        self.assert_config_equals_default(config['test'])

    def test_invalid_config(self):
        with open(os.path.join(self.config_dir, "test.conf"), "w") as f:
            f.write("""
Malformed configuration file
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # If there is a config file but it is bad, then no config is returned
        self.assertEqual(len(manager.configs), 0)

    def test_invalid_type(self):
        filename = os.path.join(self.config_dir, "test.conf")
        with open(filename, "w") as f:
            f.write("""
[test]
type=invalid
server=1.2.3.4
username=test
""")
        # Instantiating the DestinationToSourceMapper with an invalid config should not fail
        # instead we expect that the list of configs managed by the DestinationToSourceMapper does not
        # include the invalid one
        config_manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # There should be no configs parsed successfully, therefore the list of configs should
        # be empty
        self.assertEqual(len(config_manager.configs), 0)

    @unittest.skipIf(os.getuid() == 0, "Can't create unreadable file when running as root")
    def test_unreadable_config(self):
        filename = os.path.join(self.config_dir, "test.conf")
        with open(filename, "w") as f:
            f.write("""
[test]
type=esx
server=1.2.3.4
username=admin
password=password
owner=root
""")
        os.chmod(filename, 0)
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # If there is a config file but it is bad, then no config is returned
        self.assertEqual(len(manager.configs), 0)

    def test_config_duplicate_keys(self):
        filename = os.path.join(self.config_dir, "test.conf")
        with open(filename, "w") as f:
            f.write("""
[test]
type=esx
server=1.2.3.4
username=admin
password=password
owner=root
owner=tester
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        self.assertEqual(manager.configs[0][1]['owner'], 'tester')

    @patch('virtwho.password.Password._read_key_iv')
    def testCryptedPassword(self, password):
        password.return_value = (hexlify(Password._generate_key()), hexlify(Password._generate_key()))
        passwd = "TestSecretPassword!"
        crypted = hexlify(Password.encrypt(passwd)).decode('utf-8')

        filename = os.path.join(self.config_dir, "test.conf")
        with open(filename, "w") as f:
            f.write("""
[test]
type=esx
server=1.2.3.4
username=admin
encrypted_password=%s
owner=root
""" % crypted)
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        self.assertEqual(manager.configs[0][1]['password'], passwd)

    @patch('virtwho.password.Password._read_key_iv')
    def testCryptedRHSMPassword(self, password):
        password.return_value = (hexlify(Password._generate_key()), hexlify(Password._generate_key()))
        passwd = "TestSecretPassword!"
        crypted = hexlify(Password.encrypt(passwd)).decode('utf-8')

        filename = os.path.join(self.config_dir, "test.conf")
        with open(filename, "w") as f:
            f.write("""
[test]
type=esx
server=1.2.3.4
username=admin
password=bacon
rhsm_username=admin
rhsm_encrypted_password=%s
owner=root
""" % crypted)
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        self.assertEqual(manager.configs[0][1]["rhsm_password"], passwd)

    @patch('virtwho.password.Password._read_key_iv')
    def testCryptedRHSMProxyPassword(self, password):
        password.return_value = (hexlify(Password._generate_key()), hexlify(Password._generate_key()))
        passwd = "TestSecretPassword!"
        crypted = hexlify(Password.encrypt(passwd)).decode('utf-8')

        filename = os.path.join(self.config_dir, "test.conf")
        with open(filename, "w") as f:
            f.write("""
[test]
type=esx
server=1.2.3.4
username=admin
password=bacon
rhsm_encrypted_proxy_password=%s
owner=root
""" % crypted)
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        self.assertEqual(manager.configs[0][1]["rhsm_proxy_password"], passwd)

    def testCryptedPasswordWithoutKey(self):
        Password.KEYFILE = "/some/nonexistant/file"
        # passwd = "TestSecretPassword!"
        with self.assertRaises(InvalidKeyFile):
            Password.decrypt(unhexlify("06a9214036b8a15b512e03d534120006"))

    def testNoOptionsConfig(self):
        with open(os.path.join(self.config_dir, "test.conf"), "w") as f:
            f.write("""
[test]
type=esx
""")
        # Instantiating the DestinationToSourceMapper with an invalid config should not fail
        # instead we expect that the list of configs managed by the DestinationToSourceMapper does not
        # include the invalid one
        config_manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # There should be no configs parsed successfully, therefore the list of configs should
        # be empty
        self.assertEqual(len(config_manager.configs), 0)

    def testMultipleConfigsInFile(self):
        config_1 = combine_dicts(TestReadingConfigs.source_options_1,
                                 TestReadingConfigs.dest_options_1)
        config_2 = combine_dicts(TestReadingConfigs.source_options_2,
                                 TestReadingConfigs.dest_options_2)

        with open(os.path.join(self.config_dir, "test.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(config_1) +
                    TestReadingConfigs.dict_to_ini(config_2))

        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 2)
        for name, config in manager.configs:
            self.assertIn(config.name, [config_1["name"], config_2["name"]])
            if config.name == config_1['name']:
                self.assert_config_contains_all(config, config_1)
            elif config.name == config_2['name']:
                self.assert_config_contains_all(config, config_2)

    def testMultipleConfigFiles(self):
        config_1 = combine_dicts(TestReadingConfigs.source_options_1,
                                 TestReadingConfigs.dest_options_1)
        config_2 = combine_dicts(TestReadingConfigs.source_options_2,
                                 TestReadingConfigs.dest_options_2)

        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(config_1))
        with open(os.path.join(self.config_dir, "test2.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(config_2))

        expected_dest_1 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_1)
        expected_dest_2 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_2)

        expected_mapping = {
            expected_dest_1: [config_1['name']],
            expected_dest_2: [config_2['name']]
        }

        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 2)
        self.assertEqual(manager.dest_to_sources_map, expected_mapping)
        self.assertEqual(manager.dests, set([expected_dest_1, expected_dest_2]))
        self.assertEqual(manager.sources,
                         set([config_1['name'], config_2['name']]))

        result1 = manager.configs[0][1]
        result2 = manager.configs[1][1]

        self.assertIn(result1.name, ("test1", "test2"))
        if result1.name == "test2":
            result2, result1 = result1, result2

        self.assert_config_contains_all(result1, config_1)
        self.assert_config_contains_all(result2, config_2)

    def test_many_sources_to_one_dest(self):
        # This tests that there can be multiple configs that specify to
        # report to the same destination
        config_1 = combine_dicts(TestReadingConfigs.source_options_1,
                                 TestReadingConfigs.dest_options_1)
        config_2 = combine_dicts(TestReadingConfigs.source_options_2,
                                 TestReadingConfigs.dest_options_1)
        expected_dest = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_1)

        expected_mapping = {expected_dest: [config_1['name'],
                                            config_2['name']]}

        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(config_1) +
                    TestReadingConfigs.dict_to_ini(config_2))

        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(manager.dests, set([expected_dest]))
        self.assertEqual(manager.sources,
                         set([config_1['name'], config_2['name']]))

        self.assertEqual(manager.dest_to_sources_map, expected_mapping)

    def test_one_source_to_many_dests(self):
        # This tests that there can be one source that specifies
        # information for different destinations and that the correct mapping
        # is created.
        config_1 = combine_dicts(TestReadingConfigs.source_options_1,
                                 TestReadingConfigs.dest_options_1)

        # NOTE: virt-who today does not support config sections having the same
        # name. Hence the only way to have one source go to multiple
        # destinations (without new config options) is to have two sections
        # with the same information but different section names
        config_options_2 = TestReadingConfigs.source_options_1.copy()
        config_options_2['name'] = 'test2'
        config_2 = combine_dicts(config_options_2,
                                 TestReadingConfigs.dest_options_2)

        expected_dest_1 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_1)
        expected_dest_2 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_2)
        expected_mapping = {
            expected_dest_1: [config_1['name']],
            expected_dest_2: [config_2['name']]  # config_2['name'] ==
                                                 # config_1['name']
        }

        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(config_1) +
                    TestReadingConfigs.dict_to_ini(config_2))

        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(manager.dest_to_sources_map, expected_mapping)

    def test_one_source_to_one_dest(self):
        config_1 = combine_dicts(TestReadingConfigs.source_options_1,
                                 TestReadingConfigs.dest_options_1)
        expected_dest_1 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_1)
        expected_mapping = {
            expected_dest_1: [config_1['name']]
        }

        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(config_1))

        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(manager.dest_to_sources_map, expected_mapping)

    def test_two_sources_to_two_dests(self):
        config_1 = combine_dicts(TestReadingConfigs.source_options_1,
                                 TestReadingConfigs.dest_options_1)
        config_2 = combine_dicts(TestReadingConfigs.source_options_2,
                                 TestReadingConfigs.dest_options_2)
        expected_dest_1 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_1)
        expected_dest_2 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_2)
        expected_mapping = {
            expected_dest_1: [config_1['name']],
            expected_dest_2: [config_2['name']]
        }

        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(config_1) +
                    TestReadingConfigs.dict_to_ini(config_2))

        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(manager.dest_to_sources_map, expected_mapping)

    def test_many_sources_to_many_dests(self):
        config_1 = combine_dicts(TestReadingConfigs.source_options_1,
                                 TestReadingConfigs.dest_options_1)
        config_2 = combine_dicts(TestReadingConfigs.source_options_2,
                                 TestReadingConfigs.dest_options_2)

        # Create another source config that is slightly different
        source_3_options = TestReadingConfigs.source_options_2.copy()
        source_3_options['name'] = 'test3'
        source_4_options = TestReadingConfigs.source_options_1.copy()
        source_4_options['name'] = 'test4'

        # Create another dest config that is slightly different
        dest_options_3 = TestReadingConfigs.dest_options_2.copy()
        dest_options_3['owner'] = 'some_cool_owner_person'

        config_3 = combine_dicts(source_3_options,
                                 TestReadingConfigs.dest_options_2)

        config_4 = combine_dicts(source_4_options,
                                 dest_options_3)

        expected_dest_1 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_1)
        expected_dest_2 = Satellite6DestinationInfo(
                **TestReadingConfigs.dest_options_2)
        expected_dest_3 = Satellite6DestinationInfo(**dest_options_3)

        expected_mapping = {
            expected_dest_1: [config_1['name']],
            expected_dest_2: [config_2['name'], config_3['name']],
            expected_dest_3: [config_4['name']]
        }

        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write(TestReadingConfigs.dict_to_ini(config_1) +
                    TestReadingConfigs.dict_to_ini(config_2) +
                    TestReadingConfigs.dict_to_ini(config_3) +
                    TestReadingConfigs.dict_to_ini(config_4))

        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(manager.dest_to_sources_map, expected_mapping)

    def testLibvirtConfig(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=libvirt
server=1.2.3.4
username=admin
password=password
owner=root
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        config = manager.configs[0][1]
        self.assertEqual(config.name, "test1")
        self.assertEqual(config["type"], "libvirt")
        # The following server value is different than what is provided above as it has been
        # processed by the libvirt config section validation.
        # TODO decouple this from the libvirt config section (for testing only)
        self.assertEqual(config["server"], "qemu+ssh://admin@1.2.3.4/system?no_tty=1")
        self.assertEqual(config["username"], "admin")
        self.assertEqual(config["password"], "password")
        self.assertEqual(config["owner"], "root")

    def testEsxDisableSimplifiedVim(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=esx
server=1.2.3.4
username=admin
password=password
owner=root
simplified_vim=false
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        _, config = manager.configs[0]
        self.assertFalse(config['simplified_vim'])

    def testMissingOwnerOption(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=esx
server=1.2.3.4
username=admin
password=password
rhsm_hostname=abc
""")
        # Instantiating the DestinationToSourceMapper with an invalid config should not fail
        # instead we expect that the list of configs managed by the DestinationToSourceMapper does not
        # include the invalid one
        config_manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # There should be no configs parsed successfully, therefore the list of configs should
        # be empty
        self.assertEqual(len(config_manager.configs), 0)

        def testEmptySectionName(self):
            with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
                f.write("""
[]
type=esx
server=1.2.3.4
username=admin
password=password
rhsm_hostname=abc
""")
        # Instantiating the DestinationToSourceMapper with an invalid config should not fail
        # instead we expect that the list of configs managed by the DestinationToSourceMapper does not
        # include the invalid one
        config_manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # There should be no configs parsed successfully, therefore the list of configs should
        # be empty
        self.assertEqual(len(config_manager.configs), 0)

    def testWhitespaceSectionName(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[   ]
type=esx
server=1.2.3.4
username=admin
password=password
rhsm_hostname=abc
""")
        # Instantiating the DestinationToSourceMapper with an invalid config should not fail
        # instead we expect that the list of configs managed by the DestinationToSourceMapper does not
        # include the invalid one
        config_manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # There should be no configs parsed successfully, therefore the list of configs should
        # be empty
        self.assertEqual(len(config_manager.configs), 0)

    def testMissingSectionName(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
type=esx
server=1.2.3.4
username=admin
password=password
rhsm_hostname=abc
""")
        # Instantiating the DestinationToSourceMapper with an invalid config should not fail
        # instead we expect that the list of configs managed by the DestinationToSourceMapper does not
        # include the invalid one
        config_manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # There should be no configs parsed successfully, therefore the list of configs should
        # be empty
        self.assertEqual(len(config_manager.configs), 0)

    def testInvalidAndValidConfigs(self):
        valid_config_name = "valid_config"
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[%(valid_config_name)s]
type=esx
server=1.2.3.4
username=admin
password=password
owner=owner
rhsm_hostname=abc

[invalid_missing_owner]
type=esx
server=1.2.3.4
username=admin
password=password
rhsm_hostname=abc
""" % {'valid_config_name': valid_config_name})
        config_manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        # There should be only one config, and that should be the one that is valid
        self.assertEqual(len(config_manager.configs), 1)
        self.assertEqual(config_manager.configs[0][1].name, valid_config_name)

    def testCLIConfigOverridesDefaultDirectoryConfigs(self):
        cli_config_file_path = os.path.join(self.custom_config_dir, "my_file.conf")
        with open(cli_config_file_path, "w") as f:
            f.write("""
[valid_cli_section]
server=5.5.5.5
username=admin1
password=password1
owner=owner1
rhsm_hostname=abc1
""")
        cli_dict = {'configs': [cli_config_file_path]}

        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[valid_default_dir_section]
server=1.2.3.4
username=admin
password=password
owner=owner
rhsm_hostname=abc
""")
        config_manager = DestinationToSourceMapper(init_config(cli_dict, config_dir=self.config_dir))
        # There should be only one config, and that should be the one passed from the cli
        self.assertEqual(len(config_manager.configs), 1)
        config = config_manager.configs[0][1]
        self.assertEqual(config.name, "valid_cli_section")
        self.assertEqual(config["server"], "5.5.5.5")
        self.assertEqual(config["username"], "admin1")
        self.assertEqual(config["password"], "password1")
        self.assertEqual(config["owner"], "owner1")
        self.assertEqual(config["rhsm_hostname"], "abc1")

    def testCLIConfigOverridesGeneralConfigFile(self):
        cli_config_file_path = os.path.join(self.custom_config_dir, "my_file.conf")
        with open(cli_config_file_path, "w") as f:
            f.write("""
[valid_cli_section]
server=5.5.5.5
username=admin1
password=password1
owner=owner1
rhsm_hostname=abc1
""")
        cli_dict = {'configs': [cli_config_file_path]}

        # alter the main conf file constant temporarily:
        virtwho.config.VW_GENERAL_CONF_PATH = os.path.join(self.general_config_file_dir, "virt-who.conf")

        with open(virtwho.config.VW_GENERAL_CONF_PATH, "w") as f:
            f.write("""
[valid_default_main_conf_file_section]
server=1.2.3.4
username=admin
password=password
owner=owner
rhsm_hostname=abc
""")
        config_manager = DestinationToSourceMapper(init_config(cli_dict, config_dir=self.config_dir))
        # There should be only one config, and that should be the one passed from the cli
        self.assertEqual(len(config_manager.configs), 1)
        config = config_manager.configs[0][1]
        self.assertEqual(config.name, "valid_cli_section")
        self.assertEqual(config["server"], "5.5.5.5")
        self.assertEqual(config["username"], "admin1")
        self.assertEqual(config["password"], "password1")
        self.assertEqual(config["owner"], "owner1")
        self.assertEqual(config["rhsm_hostname"], "abc1")

    def testCLIConfigOverridesGeneralConfigFileButStillReadsItsGlobalAndDefaultsSections(self):
        cli_config_file_path = os.path.join(self.custom_config_dir, "my_file.conf")
        with open(cli_config_file_path, "w") as f:
            f.write("""
[valid_cli_section]
server=5.5.5.5
username=admin1
password=password1
owner=owner1
rhsm_hostname=abc1
""")
        cli_dict = {'configs': [cli_config_file_path]}

        # alter the main conf file constant temporarily:
        virtwho.config.VW_GENERAL_CONF_PATH = os.path.join(self.general_config_file_dir, "virt-who.conf")

        with open(virtwho.config.VW_GENERAL_CONF_PATH, "w") as f:
            f.write("""
[global]
interval=100
log_file=rhsm45.log

[defaults]
hypervisor_id=hostname

[valid_default_main_conf_file_section]
server=1.2.3.4
username=admin
password=password
owner=owner
rhsm_hostname=abc
""")
        config_manager = DestinationToSourceMapper(init_config(cli_dict, config_dir=self.config_dir))
        # There should be only one config, and that should be the one passed from the cli
        self.assertEqual(len(config_manager.configs), 1)
        config = config_manager.configs[0][1]
        self.assertEqual(config.name, "valid_cli_section")
        self.assertEqual(config["server"], "5.5.5.5")
        self.assertEqual(config["username"], "admin1")
        self.assertEqual(config["password"], "password1")
        self.assertEqual(config["owner"], "owner1")
        self.assertEqual(config["rhsm_hostname"], "abc1")

        # Also, check that the default section values from the VW_GENERAL_CONF_PATH file are still read
        # (and used when any of the keys are missing in the virt config)
        self.assertEqual(config["hypervisor_id"], "hostname")

        # Additionally, the global section from the VW_GENERAL_CONF_PATH file should be read in
        self.assertEqual(config_manager.effective_config["global"]["log_file"], "rhsm45.log")
        self.assertEqual(config_manager.effective_config["global"]["interval"], 100)

    def testUpdateHttpsProxyUrl(self):
        cli_config_file_path = os.path.join(self.custom_config_dir, "my_file.conf")
        with open(cli_config_file_path, "w") as f:
            f.write("""
    [valid_cli_section]
    server=5.5.5.5
    username=admin1
    password=password1
    owner=owner1
    rhsm_hostname=abc1
    """)
        cli_dict = {'configs': [cli_config_file_path]}

        # alter the main conf file constant temporarily:
        virtwho.config.VW_GENERAL_CONF_PATH = os.path.join(self.general_config_file_dir, "virt-who.conf")

        with open(virtwho.config.VW_GENERAL_CONF_PATH, "w") as f:
            f.write("""
    [global]
    interval=100
    log_file=rhsm45.log

    [system_environment]
    https_proxy=https://this.server.com

    [defaults]
    hypervisor_id=hostname

    [valid_default_main_conf_file_section]
    server=1.2.3.4
    username=admin
    password=password
    owner=owner
    rhsm_hostname=abc
    """)
        init_config(cli_dict, config_dir=self.config_dir)
        self.assertEqual(os.environ['https_proxy'], 'http://this.server.com')

    def testInvisibleConfigFile(self):
        with open(os.path.join(self.config_dir, ".test1.conf"), "w") as f:
            f.write("""
[test1]
type=libvirt
server=1.2.3.4
username=admin
password=password
owner=root
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertTrue("test1" not in [name for (name, config) in manager.configs],
                        "Hidden config file shouldn't be read")

    def testFilterHostOld(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=esx
server=1.2.3.4
username=admin
password=password
owner=root
filter_host_uuids=12345
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        self.assertEqual(manager.configs[0][1]["filter_hosts"], ['12345'])

    def testFilterHostNew(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=esx
server=1.2.3.4
username=admin
password=password
owner=root
filter_hosts=12345
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        self.assertEqual(manager.configs[0][1]["filter_hosts"], ['12345'])

    def testQuotesInConfig(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=esx
server="http://1.2.3.4"
username='admin'
password=p"asswor'd
owner=" root "
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        config = manager.configs[0][1]
        self.assertEqual(config.name, "test1")
        self.assertEqual(config["type"], "esx")
        self.assertEqual(config["server"], "http://1.2.3.4")
        self.assertEqual(config["username"], "admin")
        self.assertEqual(config["password"], "p\"asswor'd")
        self.assertEqual(config["owner"], " root ")

    def testUnicode(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=hyperv
server=http://žluťoučký servřík
username=username
password=password
owner=здравствуйте
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        config = manager.configs[0][1]
        self.assertEqual(config.name, "test1")
        self.assertEqual(config["type"], "hyperv")
        self.assertEqual(config["server"], "http://žluťoučký servřík")
        # Username and password can't be unicode, they has to be latin1 for HTTP Basic auth
        self.assertEqual(config["username"], "username")
        self.assertEqual(config["password"], "password")
        self.assertEqual(config["owner"], "здравствуйте")

    def testConfigFileExtensions(self):
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=esx
server=1.2.3.4
username=admin
password=password
owner=root
rhsm_username=rhsm_admin1
rhsm_password=rhsm_password1
rhsm_hostname=host1
rhsm_port=12341
rhsm_prefix=prefix1
rhsm_proxy_hostname=proxyhost1
rhsm_proxy_port=43211
rhsm_proxy_user=proxyuser1
rhsm_proxy_password=proxypass1
rhsm_insecure=1
""")
        with open(os.path.join(self.config_dir, "test2.conf.bk"), "w") as f:
            f.write("""
[test2]
type=hyperv
server=1.2.3.5
username=admin
password=password
owner=root
rhsm_username=rhsm_admin2
rhsm_password=rhsm_password2
rhsm_hostname=host2
rhsm_port=12342
rhsm_prefix=prefix2
rhsm_proxy_hostname=proxyhost2
rhsm_proxy_port=43212
rhsm_proxy_user=proxyuser2
rhsm_proxy_password=proxypass2
rhsm_insecure=2
""")

        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        name, config = manager.configs[0]

        self.assertEqual(name, "test1")
        # TODO decouple tests like these from the ConfigSections that they imply
        # The values used here reflect the expected output of the EsxConfigSection validation
        # (If in case these seem strange)
        self.assertEqual(config["type"], "esx")
        self.assertEqual(config["server"], "https://1.2.3.4")
        self.assertEqual(config["username"], "admin")
        self.assertEqual(config["password"], "password")
        self.assertEqual(config["owner"], "root")
        self.assertEqual(config["rhsm_username"], 'rhsm_admin1')
        self.assertEqual(config["rhsm_password"], 'rhsm_password1')
        self.assertEqual(config["rhsm_hostname"], 'host1')
        self.assertEqual(config["rhsm_port"], '12341')
        self.assertEqual(config["rhsm_prefix"], 'prefix1')
        self.assertEqual(config["rhsm_proxy_hostname"], 'proxyhost1')
        self.assertEqual(config["rhsm_proxy_port"], '43211')
        self.assertEqual(config["rhsm_proxy_user"], 'proxyuser1')
        self.assertEqual(config["rhsm_proxy_password"], 'proxypass1')
        self.assertEqual(config["rhsm_insecure"], '1')

    def testLineContinuationInConfig(self):
        """ Test that when a config line that starts with space or tab, it is treated
        as a continuation of the previous line.
        :return:
        """
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=esx
server=http://1.2.3.4
 value
username=admin
password=password
owner=root
    filter_hosts=abc.efg.com
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        config = manager.configs[0][1]
        self.assertEqual(config.name, "test1")

        self.assertEqual(config["server"], 'http://1.2.3.4\nvalue')

    @patch('logging.Logger.warn')
    def testCommentedOutLineContinuationInConfig(self, logger_warn):
        """Test that when a config line that starts with space or tab which is followed by a '#',
        if we are running python2: it is treated as a continuation of the previous line,
        but a warning is logged for the user.
        If we are running python3: it is ignored as a comment, and no warning is logged.
        :return:
        """
        with open(os.path.join(self.config_dir, "test1.conf"), "w") as f:
            f.write("""
[test1]
type=esx
server=http://1.2.3.4
 #value
username=admin
password=password
owner=root
    #filter_hosts=abc.efg.com
""")
        manager = DestinationToSourceMapper(init_config({}, config_dir=self.config_dir))
        self.assertEqual(len(manager.configs), 1)
        config = manager.configs[0][1]
        self.assertEqual(config.name, "test1")
        self.assertEqual(config["server"], "http://1.2.3.4")
        self.assertFalse(logger_warn.called)
        self.assertEqual(logger_warn.call_count, 0)


class TestParseList(TestBase):
    def test_unquoted(self):
        self.assertEqual(
            parse_list('abc,def,ghi'),
            ['abc', 'def', 'ghi']
        )
        self.assertEqual(
            parse_list(' abc, def ,ghi, jkl '),
            ['abc', 'def', 'ghi', 'jkl']
        )
        self.assertEqual(
            parse_list(' abc, def ,ghi, jkl,'),
            ['abc', 'def', 'ghi', 'jkl']
        )

    def test_doublequoted(self):
        self.assertEqual(
            parse_list('"abc","def","ghi"'),
            ['abc', 'def', 'ghi']
        )
        self.assertEqual(
            parse_list('"abc", "def" ,"ghi" , "jkl"'),
            ['abc', 'def', 'ghi', 'jkl']
        )
        self.assertEqual(
            parse_list('"abc ", " def" ,"g h i" , " j,l "'),
            ['abc ', ' def', 'g h i', ' j,l ']
        )
        self.assertRaises(ValueError, parse_list, 'abc"def')
        self.assertEqual(
            parse_list('"abc\\"", "\\"def"'),
            ['abc"', '"def']
        )

    def test_singlequoted(self):
        self.assertEqual(
            parse_list("'abc','def','ghi'"),
            ['abc', 'def', 'ghi']
        )
        self.assertEqual(
            parse_list("'abc', 'def' ,'ghi' , 'jkl'"),
            ['abc', 'def', 'ghi', 'jkl']
        )
        self.assertEqual(
            parse_list("'abc ', ' def' ,'g h i' , ' j,l '"),
            ['abc ', ' def', 'g h i', ' j,l ']
        )
        self.assertRaises(ValueError, parse_list, "abc'def")

    def test_special(self):
        self.assertEqual(
            parse_list("'\babc','!def',',\\\\ghi'"),
            ['\babc', '!def', ',\\ghi']
        )
        self.assertEqual(
            parse_list("'a\nc', '\tdef' ,'\"ghi\"' , \"'jkl'\""),
            ['a\nc', '\tdef', '"ghi"', "'jkl'"]
        )
        self.assertEqual(
            parse_list("'abc\\ ', '\\\\ def' ,'g h i' , ' jkl '"),
            ['abc ', '\\ def', 'g h i', ' jkl ']
        )


class TestSystemEnvironmentConfig(TestBase):
    """
    Test base for testing system_environment
    """
    # Values used for testing system_environment
    SYSTEM_ENVIRONMENT_SECTION_VALUES = {
        'http_proxy': 'http://this.proxy.com',
        'https_proxy': 'https://that.proxy.com',
        'no_proxy': 'not.this.proxy.com'
    }

    def test_express_system_environment_config_section(self):
        virtwho.config.parse_system_environment(self.SYSTEM_ENVIRONMENT_SECTION_VALUES)
        for key, value in self.SYSTEM_ENVIRONMENT_SECTION_VALUES.items():
            assert os.environ[key] == value
