#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Example usage and tests for :mod:`orion.core.io.resolve_config`."""

import hashlib
import logging
import os
import shutil
import socket

import git
import pytest

import orion.core
import orion.core.io.resolve_config as resolve_config


@pytest.fixture
def force_is_exe(monkeypatch):
    """Mock resolve_config to recognize any string as an executable script."""
    def is_exe(path):
        return True

    monkeypatch.setattr(resolve_config, "is_exe", is_exe)


@pytest.mark.usefixtures("empty_config")
def test_fetch_default_options():
    """Verify default options"""
    default_config = resolve_config.fetch_default_options()

    assert default_config['algorithms'] == 'random'
    assert default_config['database']['host'] == socket.gethostbyname(socket.gethostname())
    assert default_config['database']['name'] == 'orion'
    assert default_config['database']['type'] == 'MongoDB'
    assert default_config['database']['port'] == 27017

    assert default_config['max_trials'] == float('inf')
    assert default_config['name'] is None
    assert default_config['pool_size'] == 1


@pytest.mark.usefixtures("empty_config")
def test_socket_on_osx(monkeypatch):
    """Verify that default hostname is set properly on OSX"""
    default_config = resolve_config.fetch_default_options()
    assert default_config['database']['host'] == socket.gethostbyname(socket.gethostname())
    assert socket.gethostbyname(socket.gethostname()) != 'localhost'

    monkeypatch.setattr(socket, 'gethostname', lambda: 'wrong_name_on_osx')
    config = orion.core.build_config()
    resolve_config.config = config
    default_config = resolve_config.fetch_default_options()
    assert default_config['database']['host'] == 'localhost'


def test_fetch_env_vars():
    """Verify env vars are fetched properly"""
    env_vars_config = resolve_config.fetch_env_vars()
    assert env_vars_config == {'database': {}}

    db_name = "orion_test"

    os.environ['ORION_DB_NAME'] = db_name

    env_vars_config = resolve_config.fetch_env_vars()
    assert env_vars_config == {'database': {'name': 'orion_test'}}

    db_type = "MongoDB"
    os.environ['ORION_DB_TYPE'] = db_type

    env_vars_config = resolve_config.fetch_env_vars()
    assert env_vars_config == {'database': {'name': db_name, 'type': db_type}}


@pytest.mark.usefixtures("version_XYZ")
def test_fetch_metadata_orion_version():
    """Verify orion version"""
    metadata = resolve_config.fetch_metadata()
    assert metadata['orion_version'] == 'XYZ'


def test_fetch_metadata_executable_users_script(script_path):
    """Verify executable user script with absolute path"""
    metadata = resolve_config.fetch_metadata(user_args=[script_path])
    assert metadata['user_script'] == os.path.abspath(script_path)


def test_fetch_metadata_non_executable_users_script():
    """Verify executable user script keeps given path"""
    script_path = 'tests/functional/demo/orion_config.yaml'
    metadata = resolve_config.fetch_metadata(user_args=[script_path])
    assert metadata['user_script'] == script_path


def test_fetch_metadata_python_users_script(script_path):
    """Verify user script is correctly infered if called with python"""
    metadata = resolve_config.fetch_metadata(user_args=['python', script_path, 'some', 'args'])
    assert metadata['user_script'] == script_path


def test_fetch_metadata_not_existed_path():
    """Verfiy the raise of error when user_script path does not exist"""
    path = 'dummy/path'
    with pytest.raises(OSError) as exc_info:
        resolve_config.fetch_metadata(user_args=[path])
    assert "The path specified for the script does not exist" in str(exc_info.value)


@pytest.mark.usefixtures()
def test_fetch_metadata_user_args(script_path):
    """Verify user args"""
    user_args = [os.path.abspath(script_path)] + list(map(str, range(10)))
    metadata = resolve_config.fetch_metadata(user_args=user_args)
    assert metadata['user_script'] == user_args[0]
    assert metadata['user_args'] == user_args


@pytest.mark.usefixtures("with_user_tsirif")
def test_fetch_metadata_user_tsirif():
    """Verify user name"""
    metadata = resolve_config.fetch_metadata()
    assert metadata['user'] == "tsirif"


def test_fetch_metadata():
    """Verify no additional data is stored in metadata"""
    metadata = resolve_config.fetch_metadata()
    len(metadata) == 4


def test_fetch_config_from_cmdargs():
    """Verify fetch_config returns empty dict on no config file path"""
    cmdargs = {
        'name': 'test',
        'user': 'me',
        'version': 1,
        'config': None,
        'exp_max_trials': 'exp_max_trials',
        'worker_trials': 'worker_trials',
        'exp_max_broken': 'exp_max_broken',
        'working_dir': 'working_dir',
        'pool_size': 'pool_size',
        'max_trials': 'max_trials',
        'heartbeat': 'heartbeat',
        'worker_max_trials': 'worker_max_trials',
        'worker_max_broken': 'worker_max_broken',
        'max_idle_time': 'max_idle_time',
        'interrupt_signal_code': 'interrupt_signal_code',
        'user_script_config': 'user_script_config',
        'manual_resolution': 'manual_resolution',
        'non_monitored_arguments': 'non_monitored_arguments',
        'ignore_code_changes': 'ignore_code_changes',
        'auto_resolution': 'auto_resolution',
        'branch_from': 'branch_from',
        'algorithm_change': 'algorithm_change',
        'code_change_type': 'code_change_type',
        'cli_change_type': 'cli_change_type',
        'branch_to': 'branch_to',
        'config_change_type': 'config_change_type'}

    config = resolve_config.fetch_config_from_cmdargs(cmdargs)

    assert config.pop('config', None) is None

    exp_config = config.pop('experiment')
    assert exp_config.pop('name') == 'test'
    assert exp_config.pop('version') == 1
    assert exp_config.pop('user') == 'me'
    assert exp_config.pop('max_trials') == 'exp_max_trials'
    assert exp_config.pop('max_broken') == 'exp_max_broken'
    assert exp_config.pop('working_dir') == 'working_dir'
    assert exp_config.pop('pool_size') == 'pool_size'

    assert exp_config == {}

    worker_config = config.pop('worker')
    assert worker_config.pop('heartbeat') == 'heartbeat'
    assert worker_config.pop('max_trials') == 'worker_max_trials'
    assert worker_config.pop('max_broken') == 'worker_max_broken'
    assert worker_config.pop('max_idle_time') == 'max_idle_time'
    assert worker_config.pop('interrupt_signal_code') == 'interrupt_signal_code'
    assert worker_config.pop('user_script_config') == 'user_script_config'

    assert worker_config == {}

    evc_config = config.pop('evc')
    assert evc_config.pop('manual_resolution') == 'manual_resolution'
    assert evc_config.pop('non_monitored_arguments') == 'non_monitored_arguments'
    assert evc_config.pop('ignore_code_changes') == 'ignore_code_changes'
    assert evc_config.pop('auto_resolution') == 'auto_resolution'
    assert evc_config.pop('branch_from') == 'branch_from'
    assert evc_config.pop('algorithm_change') == 'algorithm_change'
    assert evc_config.pop('code_change_type') == 'code_change_type'
    assert evc_config.pop('cli_change_type') == 'cli_change_type'
    assert evc_config.pop('branch_to') == 'branch_to'
    assert evc_config.pop('config_change_type') == 'config_change_type'

    assert evc_config == {}

    assert config == {}


@pytest.mark.parametrize(
    "argument",
    ['config', 'user', 'user_args', 'name', 'version', 'branch_from', 'branch_to'])
def test_fetch_config_from_cmdargs_no_empty(argument):
    """Verify fetch_config returns only defined arguments."""
    config = resolve_config.fetch_config_from_cmdargs({})
    assert config == {}

    config = resolve_config.fetch_config_from_cmdargs({argument: None})
    assert config == {}

    config = resolve_config.fetch_config_from_cmdargs({argument: False})
    assert config == {}

    config = resolve_config.fetch_config_from_cmdargs({argument: 1})

    if argument in ['name', 'user', 'version']:
        assert config == {'experiment': {argument: 1}}
    elif argument in ['branch_from', 'branch_to']:
        assert config == {'evc': {argument: 1}}
    else:
        assert config == {argument: 1}


def test_fetch_config_no_hit():
    """Verify fetch_config returns empty dict on no config file path"""
    config = resolve_config.fetch_config({"config": ""})
    assert config == {}


def test_fetch_config(config_file):
    """Verify fetch_config returns valid dictionnary"""
    config = resolve_config.fetch_config({"config": config_file})

    assert config.pop('storage') == {
        'database': {
            'host': 'mongodb://user:pass@localhost',
            'name': 'orion_test',
            'type': 'mongodb'}}

    assert config.pop('experiment') == {
        'max_trials': 100,
        'name': 'voila_voici',
        'pool_size': 1,
        'algorithms': 'random',
        'strategy': 'NoParallelStrategy'}

    assert config == {}


def test_fetch_config_global_local_coherence(monkeypatch, config_file):
    """Verify fetch_config parses local config according to global config structure."""
    def mocked_config(file_object):
        return orion.core.config.to_dict()
    monkeypatch.setattr('yaml.safe_load', mocked_config)

    config = resolve_config.fetch_config({"config": config_file})

    # Test storage subconfig
    storage_config = config.pop('storage')
    database_config = storage_config.pop('database')
    assert storage_config.pop('type') == orion.core.config.storage.type
    assert storage_config == {}

    assert database_config.pop('host') == orion.core.config.storage.database.host
    assert database_config.pop('name') == orion.core.config.storage.database.name
    assert database_config.pop('port') == orion.core.config.storage.database.port
    assert database_config.pop('type') == orion.core.config.storage.database.type

    assert database_config == {}

    # Test experiment subconfig
    exp_config = config.pop('experiment')
    assert exp_config.pop('max_trials') == orion.core.config.experiment.max_trials
    assert exp_config.pop('max_broken') == orion.core.config.experiment.max_broken
    assert exp_config.pop('working_dir') == orion.core.config.experiment.working_dir
    assert exp_config.pop('pool_size') == orion.core.config.experiment.pool_size
    assert exp_config.pop('algorithms') == orion.core.config.experiment.algorithms
    assert exp_config.pop('strategy') == orion.core.config.experiment.strategy

    assert exp_config == {}

    # Test worker subconfig
    worker_config = config.pop('worker')
    assert worker_config.pop('heartbeat') == orion.core.config.worker.heartbeat
    assert worker_config.pop('max_trials') == orion.core.config.worker.max_trials
    assert worker_config.pop('max_broken') == orion.core.config.worker.max_broken
    assert worker_config.pop('max_idle_time') == orion.core.config.worker.max_idle_time
    assert (worker_config.pop('interrupt_signal_code') ==
            orion.core.config.worker.interrupt_signal_code)
    assert (worker_config.pop('user_script_config') ==
            orion.core.config.worker.user_script_config)

    assert worker_config == {}

    # Test evc subconfig
    evc_config = config.pop('evc')
    assert evc_config.pop('auto_resolution') == orion.core.config.evc.auto_resolution
    assert evc_config.pop('manual_resolution') == orion.core.config.evc.manual_resolution
    assert (evc_config.pop('non_monitored_arguments') ==
            orion.core.config.evc.non_monitored_arguments)
    assert evc_config.pop('ignore_code_changes') == orion.core.config.evc.ignore_code_changes
    assert evc_config.pop('algorithm_change') == orion.core.config.evc.algorithm_change
    assert evc_config.pop('code_change_type') == orion.core.config.evc.code_change_type
    assert evc_config.pop('cli_change_type') == orion.core.config.evc.cli_change_type
    assert evc_config.pop('config_change_type') == orion.core.config.evc.config_change_type

    assert evc_config == {}

    # Confirm that all fields were tested.
    assert config == {}


def test_fetch_config_dash(monkeypatch, config_file):
    """Verify fetch_config supports dash."""
    def mocked_config(file_object):
        return {'experiment': {'max-broken': 10, 'algorithms': {'dont-change': 'me'}}}
    monkeypatch.setattr('yaml.safe_load', mocked_config)

    config = resolve_config.fetch_config({"config": config_file})

    assert config == {'experiment': {'max_broken': 10, 'algorithms': {'dont-change': 'me'}}}


def test_fetch_config_underscore(monkeypatch, config_file):
    """Verify fetch_config supports underscore as well."""
    def mocked_config(file_object):
        return {'experiment': {'max_broken': 10, 'algorithms': {'dont-change': 'me'}}}
    monkeypatch.setattr('yaml.safe_load', mocked_config)

    config = resolve_config.fetch_config({"config": config_file})

    assert config == {'experiment': {'max_broken': 10, 'algorithms': {'dont-change': 'me'}}}


def test_fetch_config_deprecated_max_trials(monkeypatch, config_file):
    """Verify fetch_config will overwrite deprecated value if also properly defined."""
    def mocked_config(file_object):
        return {'experiment': {'max_trials': 10}, 'max_trials': 20}
    monkeypatch.setattr('yaml.safe_load', mocked_config)

    config = resolve_config.fetch_config({"config": config_file})

    assert config == {'experiment': {'max_trials': 10}}


def test_fetch_config_deprecate_tricky_names(monkeypatch, config_file):
    """Verify fetch_config assigns values properly for the similar names."""
    def mocked_config(file_object):
        return {
            'experiment': {
                'worker_trials': 'should_be_ignored'},
            'max_trials': 'exp_max_trials',
            'max_broken': 'exp_max_broken',
            'worker_trials': 'worker_max_trials',
            'name': 'exp_name'
        }
    monkeypatch.setattr('yaml.safe_load', mocked_config)

    config = resolve_config.fetch_config({"config": config_file})

    assert config == {
        'experiment': {
            'name': 'exp_name',
            'max_trials': 'exp_max_trials',
            'max_broken': 'exp_max_broken'},
        'worker': {'max_trials': 'worker_max_trials'}}


def test_merge_configs_update_two():
    """Ensure update on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'a': 3}

    m = resolve_config.merge_configs(a, b)

    assert m == {'a': 3, 'b': 2}


def test_merge_configs_update_three():
    """Ensure two updates on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'a': 3}
    c = {'b': 4}

    m = resolve_config.merge_configs(a, b, c)

    assert m == {'a': 3, 'b': 4}


def test_merge_configs_update_four():
    """Ensure three updates on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'a': 3}
    c = {'b': 4}
    d = {'a': 5, 'b': 6}

    m = resolve_config.merge_configs(a, b, c, d)

    assert m == {'a': 5, 'b': 6}


def test_merge_configs_extend_two():
    """Ensure extension on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'c': 3}

    m = resolve_config.merge_configs(a, b)

    assert m == {'a': 1, 'b': 2, 'c': 3}


def test_merge_configs_extend_three():
    """Ensure two extensions on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'c': 3}
    c = {'d': 4}

    m = resolve_config.merge_configs(a, b, c)

    assert m == {'a': 1, 'b': 2, 'c': 3, 'd': 4}


def test_merge_configs_extend_four():
    """Ensure three extensions on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'c': 3}
    c = {'d': 4}
    d = {'e': 5}

    m = resolve_config.merge_configs(a, b, c, d)

    assert m == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}


def test_merge_configs_update_extend_two():
    """Ensure update and extension on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'b': 3, 'c': 4}

    m = resolve_config.merge_configs(a, b)

    assert m == {'a': 1, 'b': 3, 'c': 4}


def test_merge_configs_update_extend_three():
    """Ensure two updates and extensions on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'b': 3, 'c': 4}
    c = {'a': 5, 'd': 6}

    m = resolve_config.merge_configs(a, b, c)

    assert m == {'a': 5, 'b': 3, 'c': 4, 'd': 6}


def test_merge_configs_update_extend_four():
    """Ensure three updates and extensions on first level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'b': 3, 'c': 4}
    c = {'a': 5, 'd': 6}
    d = {'d': 7, 'e': 8}

    m = resolve_config.merge_configs(a, b, c, d)

    assert m == {'a': 5, 'b': 3, 'c': 4, 'd': 7, 'e': 8}


def test_merge_sub_configs_update_two():
    """Ensure updating to second level is fine"""
    a = {'a': 1, 'b': 2}
    b = {'b': {'c': 3}}

    m = resolve_config.merge_configs(a, b)

    assert m == {'a': 1, 'b': {'c': 3}}


def test_merge_sub_configs_sub_update_two():
    """Ensure updating on second level is fine"""
    a = {'a': 1, 'b': {'c': 2}}
    b = {'b': {'c': 3}}

    m = resolve_config.merge_configs(a, b)

    assert m == {'a': 1, 'b': {'c': 3}}

    a = {'a': 1, 'b': {'c': 2, 'd': 3}}
    b = {'b': {'c': 4}}

    m = resolve_config.merge_configs(a, b)

    assert m == {'a': 1, 'b': {'c': 4, 'd': 3}}


def test_merge_sub_configs_sub_extend_two():
    """Ensure updating to third level from second level is fine"""
    a = {'a': 1, 'b': {'c': 2}}
    b = {'d': {'e': 3}}

    m = resolve_config.merge_configs(a, b)

    assert m == {'a': 1, 'b': {'c': 2}, 'd': {'e': 3}}

    a = {'a': 1, 'b': {'c': 2, 'd': 3}}
    b = {'b': {'e': {'f': 4}}}

    m = resolve_config.merge_configs(a, b)

    assert m == {'a': 1, 'b': {'c': 2, 'd': 3, 'e': {'f': 4}}}


def test_merge_sub_configs_update_three():
    """Ensure updating twice to third level from second level is fine"""
    a = {'a': 1, 'b': {'c': 2}}
    b = {'b': {'c': 3}}
    c = {'b': {'c': {'d': 4}}}

    m = resolve_config.merge_configs(a, b, c)

    assert m == {'a': 1, 'b': {'c': {'d': 4}}}

    a = {'a': 1, 'b': {'c': 2, 'd': 3}}
    b = {'b': {'c': 4}}
    c = {'b': {'c': {'e': 5}}}

    m = resolve_config.merge_configs(a, b, c)

    assert m == {'a': 1, 'b': {'c': {'e': 5}, 'd': 3}}


@pytest.fixture
def repo():
    """Create a dummy repo for the tests."""
    os.chdir('../')
    os.makedirs('dummy_orion')
    os.chdir('dummy_orion')
    repo = git.Repo.init('.')
    with open('README.md', 'w+') as f:
        f.write('dummy content')
    repo.git.add('README.md')
    repo.index.commit('initial commit')
    repo.create_head('master')
    repo.git.checkout('master')
    yield repo
    os.chdir('../')
    shutil.rmtree('dummy_orion')
    os.chdir('orion')


def test_infer_versioning_metadata_on_clean_repo(repo):
    """
    Test how `infer_versioning_metadata` fills its different fields
    when the user's repo is clean:
    `is_dirty`, `active_branch` and `diff_sha`.
    """
    vcs = resolve_config.infer_versioning_metadata('.git')
    assert not vcs['is_dirty']
    assert vcs['active_branch'] == 'master'
    # the diff should be empty so the diff_sha should be equal to the diff sha of an empty string
    assert vcs['diff_sha'] == hashlib.sha256(''.encode('utf-8')).hexdigest()


def test_infer_versioning_metadata_on_dirty_repo(repo):
    """
    Test how `infer_versioning_metadata` fills its different fields
    when the uers's repo is dirty:
    `is_dirty`, `HEAD_sha`, `active_branch` and `diff_sha`.
    """
    existing_metadata = {}
    existing_metadata['user_script'] = '.git'
    vcs = resolve_config.infer_versioning_metadata('.git')
    repo.create_head('feature')
    repo.git.checkout('feature')
    with open('README.md', 'w+') as f:
        f.write('dummy dummy content')
    vcs = resolve_config.infer_versioning_metadata('.git')
    assert vcs['is_dirty']
    assert vcs['active_branch'] == 'feature'
    assert vcs['diff_sha'] != hashlib.sha256(''.encode('utf-8')).hexdigest()
    repo.git.add('README.md')
    commit = repo.index.commit('Added dummy_file')
    vcs = resolve_config.infer_versioning_metadata('.git')
    assert not vcs['is_dirty']
    assert vcs['HEAD_sha'] == commit.hexsha
    assert vcs['diff_sha'] == hashlib.sha256(''.encode('utf-8')).hexdigest()


def test_fetch_user_repo_on_non_repo(caplog):
    """Test if `fetch_user_repo` logs a warning when user's script is not a git repo."""
    with caplog.at_level(logging.WARNING):
        resolve_config.fetch_user_repo('.')
    messages = [rec.message for rec in caplog.records]
    assert len(messages) == 1
    assert "Script {} is not in a git repository".format(os.getcwd()) in messages[0]


def test_infer_versioning_metadata_on_detached_head(repo):
    """Test in the case of a detached head."""
    with open('README.md', 'w+') as f:
        f.write('dummy contentt')
    repo.git.add('README.md')
    repo.index.commit('2nd commit')
    existing_metadata = {}
    existing_metadata['user_script'] = '.git'
    repo.head.reference = repo.commit('HEAD~1')
    assert repo.head.is_detached
    vcs = resolve_config.infer_versioning_metadata('.git')
    assert vcs['active_branch'] is None
