"""Test config utils."""
# pylint: disable=protected-access
import asyncio
import copy
import os
import unittest.mock as mock
from collections import OrderedDict
from ipaddress import ip_network

import asynctest
import pytest
from voluptuous import MultipleInvalid, Invalid
import yaml

from homeassistant.core import SOURCE_STORAGE, HomeAssistantError
import homeassistant.config as config_util
from homeassistant.loader import async_get_integration
from homeassistant.const import (
    ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE,
    CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME,
    CONF_CUSTOMIZE, __version__,
    CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT,
    CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES)
from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import SECRET_YAML
from homeassistant.helpers.entity import Entity
from homeassistant.components.config.group import (
    CONFIG_PATH as GROUP_CONFIG_PATH)
from homeassistant.components.config.automation import (
    CONFIG_PATH as AUTOMATIONS_CONFIG_PATH)
from homeassistant.components.config.script import (
    CONFIG_PATH as SCRIPTS_CONFIG_PATH)
import homeassistant.scripts.check_config as check_config

from tests.common import (
    get_test_config_dir, patch_yaml_files)

CONFIG_DIR = get_test_config_dir()
YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE)
SECRET_PATH = os.path.join(CONFIG_DIR, SECRET_YAML)
VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE)
GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH)
AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH)
SCRIPTS_PATH = os.path.join(CONFIG_DIR, SCRIPTS_CONFIG_PATH)
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE


def create_file(path):
    """Create an empty file."""
    with open(path, 'w'):
        pass


def teardown():
    """Clean up."""
    dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE

    if os.path.isfile(YAML_PATH):
        os.remove(YAML_PATH)

    if os.path.isfile(SECRET_PATH):
        os.remove(SECRET_PATH)

    if os.path.isfile(VERSION_PATH):
        os.remove(VERSION_PATH)

    if os.path.isfile(GROUP_PATH):
        os.remove(GROUP_PATH)

    if os.path.isfile(AUTOMATIONS_PATH):
        os.remove(AUTOMATIONS_PATH)

    if os.path.isfile(SCRIPTS_PATH):
        os.remove(SCRIPTS_PATH)


async def test_create_default_config(hass):
    """Test creation of default config."""
    await config_util.async_create_default_config(hass, CONFIG_DIR)

    assert os.path.isfile(YAML_PATH)
    assert os.path.isfile(SECRET_PATH)
    assert os.path.isfile(VERSION_PATH)
    assert os.path.isfile(GROUP_PATH)
    assert os.path.isfile(AUTOMATIONS_PATH)


def test_find_config_file_yaml():
    """Test if it finds a YAML config file."""
    create_file(YAML_PATH)

    assert YAML_PATH == config_util.find_config_file(CONFIG_DIR)


async def test_ensure_config_exists_creates_config(hass):
    """Test that calling ensure_config_exists.

    If not creates a new config file.
    """
    with mock.patch('builtins.print') as mock_print:
        await config_util.async_ensure_config_exists(hass, CONFIG_DIR)

    assert os.path.isfile(YAML_PATH)
    assert mock_print.called


async def test_ensure_config_exists_uses_existing_config(hass):
    """Test that calling ensure_config_exists uses existing config."""
    create_file(YAML_PATH)
    await config_util.async_ensure_config_exists(hass, CONFIG_DIR)

    with open(YAML_PATH) as f:
        content = f.read()

    # File created with create_file are empty
    assert content == ''


def test_load_yaml_config_converts_empty_files_to_dict():
    """Test that loading an empty file returns an empty dict."""
    create_file(YAML_PATH)

    assert isinstance(config_util.load_yaml_config_file(YAML_PATH), dict)


def test_load_yaml_config_raises_error_if_not_dict():
    """Test error raised when YAML file is not a dict."""
    with open(YAML_PATH, 'w') as f:
        f.write('5')

    with pytest.raises(HomeAssistantError):
        config_util.load_yaml_config_file(YAML_PATH)


def test_load_yaml_config_raises_error_if_malformed_yaml():
    """Test error raised if invalid YAML."""
    with open(YAML_PATH, 'w') as f:
        f.write(':')

    with pytest.raises(HomeAssistantError):
        config_util.load_yaml_config_file(YAML_PATH)


def test_load_yaml_config_raises_error_if_unsafe_yaml():
    """Test error raised if unsafe YAML."""
    with open(YAML_PATH, 'w') as f:
        f.write('hello: !!python/object/apply:os.system')

    with pytest.raises(HomeAssistantError):
        config_util.load_yaml_config_file(YAML_PATH)


def test_load_yaml_config_preserves_key_order():
    """Test removal of library."""
    with open(YAML_PATH, 'w') as f:
        f.write('hello: 2\n')
        f.write('world: 1\n')

    assert [('hello', 2), ('world', 1)] == \
        list(config_util.load_yaml_config_file(YAML_PATH).items())


async def test_create_default_config_returns_none_if_write_error(hass):
    """Test the writing of a default configuration.

    Non existing folder returns None.
    """
    with mock.patch('builtins.print') as mock_print:
        assert await config_util.async_create_default_config(
            hass, os.path.join(CONFIG_DIR, 'non_existing_dir/')) is None
    assert mock_print.called


def test_core_config_schema():
    """Test core config schema."""
    for value in (
            {CONF_UNIT_SYSTEM: 'K'},
            {'time_zone': 'non-exist'},
            {'latitude': '91'},
            {'longitude': -181},
            {'customize': 'bla'},
            {'customize': {'light.sensor': 100}},
            {'customize': {'entity_id': []}},
    ):
        with pytest.raises(MultipleInvalid):
            config_util.CORE_CONFIG_SCHEMA(value)

    config_util.CORE_CONFIG_SCHEMA({
        'name': 'Test name',
        'latitude': '-23.45',
        'longitude': '123.45',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
        'customize': {
            'sensor.temperature': {
                'hidden': True,
            },
        },
    })


def test_customize_dict_schema():
    """Test basic customize config validation."""
    values = (
        {ATTR_FRIENDLY_NAME: None},
        {ATTR_HIDDEN: '2'},
        {ATTR_ASSUMED_STATE: '2'},
    )

    for val in values:
        print(val)
        with pytest.raises(MultipleInvalid):
            config_util.CUSTOMIZE_DICT_SCHEMA(val)

    assert config_util.CUSTOMIZE_DICT_SCHEMA({
        ATTR_FRIENDLY_NAME: 2,
        ATTR_HIDDEN: '1',
        ATTR_ASSUMED_STATE: '0',
    }) == {
        ATTR_FRIENDLY_NAME: '2',
        ATTR_HIDDEN: True,
        ATTR_ASSUMED_STATE: False
    }


def test_customize_glob_is_ordered():
    """Test that customize_glob preserves order."""
    conf = config_util.CORE_CONFIG_SCHEMA(
        {'customize_glob': OrderedDict()})
    assert isinstance(conf['customize_glob'], OrderedDict)


async def _compute_state(hass, config):
    await config_util.async_process_ha_core_config(hass, config)

    entity = Entity()
    entity.entity_id = 'test.test'
    entity.hass = hass
    entity.schedule_update_ha_state()

    await hass.async_block_till_done()

    return hass.states.get('test.test')


async def test_entity_customization(hass):
    """Test entity customization through configuration."""
    config = {CONF_LATITUDE: 50,
              CONF_LONGITUDE: 50,
              CONF_NAME: 'Test',
              CONF_CUSTOMIZE: {'test.test': {'hidden': True}}}

    state = await _compute_state(hass, config)

    assert state.attributes['hidden']


@mock.patch('homeassistant.config.shutil')
@mock.patch('homeassistant.config.os')
@mock.patch('homeassistant.config.is_docker_env', return_value=False)
def test_remove_lib_on_upgrade(mock_docker, mock_os, mock_shutil, hass):
    """Test removal of library on upgrade from before 0.50."""
    ha_version = '0.49.0'
    mock_os.path.isdir = mock.Mock(return_value=True)
    mock_open = mock.mock_open()
    with mock.patch('homeassistant.config.open', mock_open, create=True):
        opened_file = mock_open.return_value
        # pylint: disable=no-member
        opened_file.readline.return_value = ha_version
        hass.config.path = mock.Mock()
        config_util.process_ha_config_upgrade(hass)
        hass_path = hass.config.path.return_value

        assert mock_os.path.isdir.call_count == 1
        assert mock_os.path.isdir.call_args == mock.call(hass_path)
        assert mock_shutil.rmtree.call_count == 1
        assert mock_shutil.rmtree.call_args == mock.call(hass_path)


@mock.patch('homeassistant.config.shutil')
@mock.patch('homeassistant.config.os')
@mock.patch('homeassistant.config.is_docker_env', return_value=True)
def test_remove_lib_on_upgrade_94(mock_docker, mock_os, mock_shutil, hass):
    """Test removal of library on upgrade from before 0.94 and in Docker."""
    ha_version = '0.93.0.dev0'
    mock_os.path.isdir = mock.Mock(return_value=True)
    mock_open = mock.mock_open()
    with mock.patch('homeassistant.config.open', mock_open, create=True):
        opened_file = mock_open.return_value
        # pylint: disable=no-member
        opened_file.readline.return_value = ha_version
        hass.config.path = mock.Mock()
        config_util.process_ha_config_upgrade(hass)
        hass_path = hass.config.path.return_value

        assert mock_os.path.isdir.call_count == 1
        assert mock_os.path.isdir.call_args == mock.call(hass_path)
        assert mock_shutil.rmtree.call_count == 1
        assert mock_shutil.rmtree.call_args == mock.call(hass_path)


def test_process_config_upgrade(hass):
    """Test update of version on upgrade."""
    ha_version = '0.92.0'

    mock_open = mock.mock_open()
    with mock.patch('homeassistant.config.open', mock_open, create=True), \
            mock.patch.object(config_util, '__version__', '0.91.0'):
        opened_file = mock_open.return_value
        # pylint: disable=no-member
        opened_file.readline.return_value = ha_version

        config_util.process_ha_config_upgrade(hass)

        assert opened_file.write.call_count == 1
        assert opened_file.write.call_args == mock.call('0.91.0')


def test_config_upgrade_same_version(hass):
    """Test no update of version on no upgrade."""
    ha_version = __version__

    mock_open = mock.mock_open()
    with mock.patch('homeassistant.config.open', mock_open, create=True):
        opened_file = mock_open.return_value
        # pylint: disable=no-member
        opened_file.readline.return_value = ha_version

        config_util.process_ha_config_upgrade(hass)

        assert opened_file.write.call_count == 0


@mock.patch('homeassistant.config.find_config_file', mock.Mock())
def test_config_upgrade_no_file(hass):
    """Test update of version on upgrade, with no version file."""
    mock_open = mock.mock_open()
    mock_open.side_effect = [FileNotFoundError(),
                             mock.DEFAULT,
                             mock.DEFAULT]
    with mock.patch('homeassistant.config.open', mock_open, create=True):
        opened_file = mock_open.return_value
        # pylint: disable=no-member
        config_util.process_ha_config_upgrade(hass)
        assert opened_file.write.call_count == 1
        assert opened_file.write.call_args == mock.call(__version__)


@mock.patch('homeassistant.config.shutil')
@mock.patch('homeassistant.config.os')
@mock.patch('homeassistant.config.find_config_file', mock.Mock())
def test_migrate_file_on_upgrade(mock_os, mock_shutil, hass):
    """Test migrate of config files on upgrade."""
    ha_version = '0.7.0'

    mock_os.path.isdir = mock.Mock(return_value=True)

    mock_open = mock.mock_open()

    def _mock_isfile(filename):
        return True

    with mock.patch('homeassistant.config.open', mock_open, create=True), \
            mock.patch('homeassistant.config.os.path.isfile', _mock_isfile):
        opened_file = mock_open.return_value
        # pylint: disable=no-member
        opened_file.readline.return_value = ha_version

        hass.config.path = mock.Mock()

        config_util.process_ha_config_upgrade(hass)

    assert mock_os.rename.call_count == 1


@mock.patch('homeassistant.config.shutil')
@mock.patch('homeassistant.config.os')
@mock.patch('homeassistant.config.find_config_file', mock.Mock())
def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass):
    """Test not migrating config files on upgrade."""
    ha_version = '0.7.0'

    mock_os.path.isdir = mock.Mock(return_value=True)

    mock_open = mock.mock_open()

    def _mock_isfile(filename):
        return False

    with mock.patch('homeassistant.config.open', mock_open, create=True), \
            mock.patch('homeassistant.config.os.path.isfile', _mock_isfile):
        opened_file = mock_open.return_value
        # pylint: disable=no-member
        opened_file.readline.return_value = ha_version

        hass.config.path = mock.Mock()

        config_util.process_ha_config_upgrade(hass)

    assert mock_os.rename.call_count == 0


async def test_loading_configuration_from_storage(hass, hass_storage):
    """Test loading core config onto hass object."""
    hass_storage["core.config"] = {
        'data': {
            'elevation': 10,
            'latitude': 55,
            'location_name': 'Home',
            'longitude': 13,
            'time_zone': 'Europe/Copenhagen',
            'unit_system': 'metric'
            },
        'key': 'core.config',
        'version': 1
    }
    await config_util.async_process_ha_core_config(
        hass, {'whitelist_external_dirs': '/tmp'})

    assert hass.config.latitude == 55
    assert hass.config.longitude == 13
    assert hass.config.elevation == 10
    assert hass.config.location_name == 'Home'
    assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC
    assert hass.config.time_zone.zone == 'Europe/Copenhagen'
    assert len(hass.config.whitelist_external_dirs) == 2
    assert '/tmp' in hass.config.whitelist_external_dirs
    assert hass.config.config_source == SOURCE_STORAGE


async def test_updating_configuration(hass, hass_storage):
    """Test updating configuration stores the new configuration."""
    core_data = {
        'data': {
            'elevation': 10,
            'latitude': 55,
            'location_name': 'Home',
            'longitude': 13,
            'time_zone': 'Europe/Copenhagen',
            'unit_system': 'metric'
            },
        'key': 'core.config',
        'version': 1
    }
    hass_storage["core.config"] = dict(core_data)
    await config_util.async_process_ha_core_config(
        hass, {'whitelist_external_dirs': '/tmp'})
    await hass.config.async_update(latitude=50)

    new_core_data = copy.deepcopy(core_data)
    new_core_data['data']['latitude'] = 50
    assert hass_storage["core.config"] == new_core_data
    assert hass.config.latitude == 50


async def test_override_stored_configuration(hass, hass_storage):
    """Test loading core and YAML config onto hass object."""
    hass_storage["core.config"] = {
        'data': {
            'elevation': 10,
            'latitude': 55,
            'location_name': 'Home',
            'longitude': 13,
            'time_zone': 'Europe/Copenhagen',
            'unit_system': 'metric'
            },
        'key': 'core.config',
        'version': 1
    }
    await config_util.async_process_ha_core_config(hass, {
        'latitude': 60,
        'whitelist_external_dirs': '/tmp',
    })

    assert hass.config.latitude == 60
    assert hass.config.longitude == 13
    assert hass.config.elevation == 10
    assert hass.config.location_name == 'Home'
    assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC
    assert hass.config.time_zone.zone == 'Europe/Copenhagen'
    assert len(hass.config.whitelist_external_dirs) == 2
    assert '/tmp' in hass.config.whitelist_external_dirs
    assert hass.config.config_source == config_util.SOURCE_YAML


async def test_loading_configuration(hass):
    """Test loading core config onto hass object."""
    await config_util.async_process_ha_core_config(hass, {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'America/New_York',
        'whitelist_external_dirs': '/tmp',
    })

    assert hass.config.latitude == 60
    assert hass.config.longitude == 50
    assert hass.config.elevation == 25
    assert hass.config.location_name == 'Huis'
    assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL
    assert hass.config.time_zone.zone == 'America/New_York'
    assert len(hass.config.whitelist_external_dirs) == 2
    assert '/tmp' in hass.config.whitelist_external_dirs
    assert hass.config.config_source == config_util.SOURCE_YAML


async def test_loading_configuration_temperature_unit(hass):
    """Test backward compatibility when loading core config."""
    await config_util.async_process_ha_core_config(hass, {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_TEMPERATURE_UNIT: 'C',
        'time_zone': 'America/New_York',
    })

    assert hass.config.latitude == 60
    assert hass.config.longitude == 50
    assert hass.config.elevation == 25
    assert hass.config.location_name == 'Huis'
    assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC
    assert hass.config.time_zone.zone == 'America/New_York'
    assert hass.config.config_source == config_util.SOURCE_YAML


async def test_loading_configuration_from_packages(hass):
    """Test loading packages config onto hass object config."""
    await config_util.async_process_ha_core_config(hass, {
        'latitude': 39,
        'longitude': -1,
        'elevation': 500,
        'name': 'Huis',
        CONF_TEMPERATURE_UNIT: 'C',
        'time_zone': 'Europe/Madrid',
        'packages': {
            'package_1': {'wake_on_lan': None},
            'package_2': {'light': {'platform': 'hue'},
                          'media_extractor': None,
                          'sun': None}},
    })

    # Empty packages not allowed
    with pytest.raises(MultipleInvalid):
        await config_util.async_process_ha_core_config(hass, {
            'latitude': 39,
            'longitude': -1,
            'elevation': 500,
            'name': 'Huis',
            CONF_TEMPERATURE_UNIT: 'C',
            'time_zone': 'Europe/Madrid',
            'packages': {'empty_package': None},
        })


@asynctest.mock.patch(
    'homeassistant.scripts.check_config.check_ha_config_file')
async def test_check_ha_config_file_correct(mock_check, hass):
    """Check that restart propagates to stop."""
    mock_check.return_value = check_config.HomeAssistantConfig()
    assert await config_util.async_check_ha_config_file(hass) is None


@asynctest.mock.patch(
    'homeassistant.scripts.check_config.check_ha_config_file')
async def test_check_ha_config_file_wrong(mock_check, hass):
    """Check that restart with a bad config doesn't propagate to stop."""
    mock_check.return_value = check_config.HomeAssistantConfig()
    mock_check.return_value.add_error("bad")

    assert await config_util.async_check_ha_config_file(hass) == 'bad'


@asynctest.mock.patch('homeassistant.config.os.path.isfile',
                      mock.Mock(return_value=True))
async def test_async_hass_config_yaml_merge(merge_log_err, hass):
    """Test merge during async config reload."""
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: {
            'pack_dict': {
                'input_boolean': {'ib1': None}}}},
        'input_boolean': {'ib2': None},
        'light': {'platform': 'test'}
    }

    files = {config_util.YAML_CONFIG_FILE: yaml.dump(config)}
    with patch_yaml_files(files, True):
        conf = await config_util.async_hass_config_yaml(hass)

    assert merge_log_err.call_count == 0
    assert conf[config_util.CONF_CORE].get(config_util.CONF_PACKAGES) \
        is not None
    assert len(conf) == 3
    assert len(conf['input_boolean']) == 2
    assert len(conf['light']) == 1


# pylint: disable=redefined-outer-name
@pytest.fixture
def merge_log_err(hass):
    """Patch _merge_log_error from packages."""
    with mock.patch('homeassistant.config._LOGGER.error') \
            as logerr:
        yield logerr


async def test_merge(merge_log_err, hass):
    """Test if we can merge packages."""
    packages = {
        'pack_dict': {'input_boolean': {'ib1': None}},
        'pack_11': {'input_select': {'is1': None}},
        'pack_list': {'light': {'platform': 'test'}},
        'pack_list2': {'light': [{'platform': 'test'}]},
        'pack_none': {'wake_on_lan': None},
    }
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'input_boolean': {'ib2': None},
        'light': {'platform': 'test'}
    }
    await config_util.merge_packages_config(hass, config, packages)

    assert merge_log_err.call_count == 0
    assert len(config) == 5
    assert len(config['input_boolean']) == 2
    assert len(config['input_select']) == 1
    assert len(config['light']) == 3
    assert isinstance(config['wake_on_lan'], OrderedDict)


async def test_merge_try_falsy(merge_log_err, hass):
    """Ensure we dont add falsy items like empty OrderedDict() to list."""
    packages = {
        'pack_falsy_to_lst': {'automation': OrderedDict()},
        'pack_list2': {'light': OrderedDict()},
    }
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'automation': {'do': 'something'},
        'light': {'some': 'light'},
    }
    await config_util.merge_packages_config(hass, config, packages)

    assert merge_log_err.call_count == 0
    assert len(config) == 3
    assert len(config['automation']) == 1
    assert len(config['light']) == 1


async def test_merge_new(merge_log_err, hass):
    """Test adding new components to outer scope."""
    packages = {
        'pack_1': {'light': [{'platform': 'one'}]},
        'pack_11': {'input_select': {'ib1': None}},
        'pack_2': {
            'light': {'platform': 'one'},
            'panel_custom': {'pan1': None},
            'api': {}},
    }
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
    }
    await config_util.merge_packages_config(hass, config, packages)

    assert merge_log_err.call_count == 0
    assert 'api' in config
    assert len(config) == 5
    assert len(config['light']) == 2
    assert len(config['panel_custom']) == 1


async def test_merge_type_mismatch(merge_log_err, hass):
    """Test if we have a type mismatch for packages."""
    packages = {
        'pack_1': {'input_boolean': [{'ib1': None}]},
        'pack_11': {'input_select': {'ib1': None}},
        'pack_2': {'light': {'ib1': None}},  # light gets merged - ensure_list
    }
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'input_boolean': {'ib2': None},
        'input_select': [{'ib2': None}],
        'light': [{'platform': 'two'}]
    }
    await config_util.merge_packages_config(hass, config, packages)

    assert merge_log_err.call_count == 2
    assert len(config) == 4
    assert len(config['input_boolean']) == 1
    assert len(config['light']) == 2


async def test_merge_once_only_keys(merge_log_err, hass):
    """Test if we have a merge for a comp that may occur only once. Keys."""
    packages = {'pack_2': {'api': None}}
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'api': None,
    }
    await config_util.merge_packages_config(hass, config, packages)
    assert config['api'] == OrderedDict()

    packages = {'pack_2': {'api': {
        'key_3': 3,
    }}}
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'api': {
            'key_1': 1,
            'key_2': 2,
        }
    }
    await config_util.merge_packages_config(hass, config, packages)
    assert config['api'] == {'key_1': 1, 'key_2': 2, 'key_3': 3, }

    # Duplicate keys error
    packages = {'pack_2': {'api': {
        'key': 2,
    }}}
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'api': {'key': 1, }
    }
    await config_util.merge_packages_config(hass, config, packages)
    assert merge_log_err.call_count == 1


async def test_merge_once_only_lists(hass):
    """Test if we have a merge for a comp that may occur only once. Lists."""
    packages = {'pack_2': {'api': {
        'list_1': ['item_2', 'item_3'],
        'list_2': ['item_1'],
        'list_3': [],
    }}}
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'api': {
            'list_1': ['item_1'],
        }
    }
    await config_util.merge_packages_config(hass, config, packages)
    assert config['api'] == {
        'list_1': ['item_1', 'item_2', 'item_3'],
        'list_2': ['item_1'],
    }


async def test_merge_once_only_dictionaries(hass):
    """Test if we have a merge for a comp that may occur only once. Dicts."""
    packages = {'pack_2': {'api': {
        'dict_1': {
            'key_2': 2,
            'dict_1.1': {'key_1.2': 1.2, },
        },
        'dict_2': {'key_1': 1, },
        'dict_3': {},
    }}}
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'api': {
            'dict_1': {
                'key_1': 1,
                'dict_1.1': {'key_1.1': 1.1, }
            },
        }
    }
    await config_util.merge_packages_config(hass, config, packages)
    assert config['api'] == {
        'dict_1': {
            'key_1': 1,
            'key_2': 2,
            'dict_1.1': {'key_1.1': 1.1, 'key_1.2': 1.2, },
        },
        'dict_2': {'key_1': 1, },
    }


async def test_merge_id_schema(hass):
    """Test if we identify the config schemas correctly."""
    types = {
        'panel_custom': 'list',
        'group': 'dict',
        'script': 'dict',
        'input_boolean': 'dict',
        'shell_command': 'dict',
        'qwikswitch': 'dict',
    }
    for domain, expected_type in types.items():
        integration = await async_get_integration(hass, domain)
        module = integration.get_component()
        typ, _ = config_util._identify_config_schema(module)
        assert typ == expected_type, "{} expected {}, got {}".format(
            domain, expected_type, typ)


async def test_merge_duplicate_keys(merge_log_err, hass):
    """Test if keys in dicts are duplicates."""
    packages = {
        'pack_1': {'input_select': {'ib1': None}},
    }
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
        'input_select': {'ib1': 1},
    }
    await config_util.merge_packages_config(hass, config, packages)

    assert merge_log_err.call_count == 1
    assert len(config) == 2
    assert len(config['input_select']) == 1


@asyncio.coroutine
def test_merge_customize(hass):
    """Test loading core config onto hass object."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
        'customize': {'a.a': {'friendly_name': 'A'}},
        'packages': {'pkg1': {'homeassistant': {'customize': {
            'b.b': {'friendly_name': 'BB'}}}}},
    }
    yield from config_util.async_process_ha_core_config(hass, core_config)

    assert hass.data[config_util.DATA_CUSTOMIZE].get('b.b') == \
        {'friendly_name': 'BB'}


async def test_auth_provider_config(hass):
    """Test loading auth provider config onto hass object."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
        CONF_AUTH_PROVIDERS: [
            {'type': 'homeassistant'},
            {'type': 'legacy_api_password', 'api_password': 'some-pass'},
        ],
        CONF_AUTH_MFA_MODULES: [
            {'type': 'totp'},
            {'type': 'totp', 'id': 'second'},
        ]
    }
    if hasattr(hass, 'auth'):
        del hass.auth
    await config_util.async_process_ha_core_config(hass, core_config)

    assert len(hass.auth.auth_providers) == 2
    assert hass.auth.auth_providers[0].type == 'homeassistant'
    assert hass.auth.auth_providers[1].type == 'legacy_api_password'
    assert len(hass.auth.auth_mfa_modules) == 2
    assert hass.auth.auth_mfa_modules[0].id == 'totp'
    assert hass.auth.auth_mfa_modules[1].id == 'second'


async def test_auth_provider_config_default(hass):
    """Test loading default auth provider config."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
    }
    if hasattr(hass, 'auth'):
        del hass.auth
    await config_util.async_process_ha_core_config(hass, core_config)

    assert len(hass.auth.auth_providers) == 1
    assert hass.auth.auth_providers[0].type == 'homeassistant'
    assert len(hass.auth.auth_mfa_modules) == 1
    assert hass.auth.auth_mfa_modules[0].id == 'totp'


async def test_auth_provider_config_default_api_password(hass):
    """Test loading default auth provider config with api password."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
    }
    if hasattr(hass, 'auth'):
        del hass.auth
    await config_util.async_process_ha_core_config(hass, core_config, 'pass')

    assert len(hass.auth.auth_providers) == 2
    assert hass.auth.auth_providers[0].type == 'homeassistant'
    assert hass.auth.auth_providers[1].type == 'legacy_api_password'
    assert hass.auth.auth_providers[1].api_password == 'pass'


async def test_auth_provider_config_default_trusted_networks(hass):
    """Test loading default auth provider config with trusted networks."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
    }
    if hasattr(hass, 'auth'):
        del hass.auth
    await config_util.async_process_ha_core_config(
        hass, core_config, trusted_networks=['192.168.0.1'])

    assert len(hass.auth.auth_providers) == 2
    assert hass.auth.auth_providers[0].type == 'homeassistant'
    assert hass.auth.auth_providers[1].type == 'trusted_networks'
    assert hass.auth.auth_providers[1].trusted_networks[0] == ip_network(
        '192.168.0.1')


async def test_disallowed_auth_provider_config(hass):
    """Test loading insecure example auth provider is disallowed."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
        CONF_AUTH_PROVIDERS: [{
            'type': 'insecure_example',
            'users': [{
                'username': 'test-user',
                'password': 'test-pass',
                'name': 'Test Name'
            }],
        }]
    }
    with pytest.raises(Invalid):
        await config_util.async_process_ha_core_config(hass, core_config)


async def test_disallowed_duplicated_auth_provider_config(hass):
    """Test loading insecure example auth provider is disallowed."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
        CONF_AUTH_PROVIDERS: [{
            'type': 'homeassistant',
        }, {
            'type': 'homeassistant',
        }]
    }
    with pytest.raises(Invalid):
        await config_util.async_process_ha_core_config(hass, core_config)


async def test_disallowed_auth_mfa_module_config(hass):
    """Test loading insecure example auth mfa module is disallowed."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
        CONF_AUTH_MFA_MODULES: [{
            'type': 'insecure_example',
            'data': [{
                'user_id': 'mock-user',
                'pin': 'test-pin'
            }]
        }]
    }
    with pytest.raises(Invalid):
        await config_util.async_process_ha_core_config(hass, core_config)


async def test_disallowed_duplicated_auth_mfa_module_config(hass):
    """Test loading insecure example auth mfa module is disallowed."""
    core_config = {
        'latitude': 60,
        'longitude': 50,
        'elevation': 25,
        'name': 'Huis',
        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
        'time_zone': 'GMT',
        CONF_AUTH_MFA_MODULES: [{
            'type': 'totp',
        }, {
            'type': 'totp',
        }]
    }
    with pytest.raises(Invalid):
        await config_util.async_process_ha_core_config(hass, core_config)


async def test_merge_split_component_definition(hass):
    """Test components with trailing description in packages are merged."""
    packages = {
        'pack_1': {'light one': {'l1': None}},
        'pack_2': {'light two': {'l2': None},
                   'light three': {'l3': None}},
    }
    config = {
        config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
    }
    await config_util.merge_packages_config(hass, config, packages)

    assert len(config) == 4
    assert len(config['light one']) == 1
    assert len(config['light two']) == 1
    assert len(config['light three']) == 1