# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright © 2016, Continuum Analytics, Inc. All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
# ----------------------------------------------------------------------------
from __future__ import absolute_import

import os

import pytest

from conda_kapsel.internal.test.tmpfile_utils import (with_directory_contents, tmp_script_commandline,
                                                      with_directory_contents_completing_project_file)
from conda_kapsel.local_state_file import LocalStateFile, DEFAULT_LOCAL_STATE_FILENAME
from conda_kapsel.plugins.provider import (Provider, ProvideContext, EnvVarProvider, ProvideResult,
                                           shutdown_service_run_state)
from conda_kapsel.plugins.registry import PluginRegistry
from conda_kapsel.plugins.requirement import EnvVarRequirement, UserConfigOverrides
from conda_kapsel.project import Project
from conda_kapsel.provide import PROVIDE_MODE_DEVELOPMENT
from conda_kapsel.project_file import DEFAULT_PROJECT_FILENAME
from conda_kapsel.prepare import (prepare_without_interaction, unprepare)
from conda_kapsel.test.project_utils import project_no_dedicated_env
from conda_kapsel.test.environ_utils import minimal_environ
from conda_kapsel.internal import (keyring, conda_api)

conda_env_var = conda_api.conda_prefix_variable()


def test_find_provider_by_class_name():
    registry = PluginRegistry()
    found = registry.find_provider_by_class_name(class_name="CondaEnvProvider")
    assert found is not None
    assert found.__class__.__name__ == "CondaEnvProvider"


def test_find_provider_by_class_name_not_found():
    registry = PluginRegistry()
    found = registry.find_provider_by_class_name(class_name="NotAThing")
    assert found is None


def test_provider_default_method_implementations():
    class UselessProvider(Provider):
        @property
        def title(self):
            return ""

        def read_config(self, requirement, environ, local_state_file, default_env_spec_name, overrides):
            return dict()

        def provide(self, requirement, context):
            pass

        def unprovide(self, requirement, local_state_file, requirement_status=None):
            pass

        def missing_env_vars_to_configure(self, requirement, environ, local_state_file):
            return ()

        def missing_env_vars_to_provide(self, requirement, environ, local_state_file):
            return ()

    provider = UselessProvider()
    # this method is supposed to do nothing by default (ignore
    # unknown names, in particular)
    provider.set_config_values_as_strings(requirement=None,
                                          environ=None,
                                          local_state_file=None,
                                          default_env_spec_name='default',
                                          overrides=None,
                                          values=dict())
    # this is supposed to return None by default
    provider.config_html(requirement=None, environ=None, local_state_file=None, overrides=None, status=None) is None


def _load_env_var_requirement(dirname, env_var):
    project = Project(dirname)

    for requirement in project.requirements:
        if isinstance(requirement, EnvVarRequirement) and requirement.env_var == env_var:
            return requirement
    assert [] == project.problems
    raise RuntimeError("No requirement for %s was in the project file, only %r" % (env_var, project.requirements))


def test_env_var_provider_with_no_value():
    def check_env_var_provider(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO")
        local_state_file = LocalStateFile.load_for_directory(dirname)
        status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides())
        context = ProvideContext(environ=dict(),
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)

        provider.provide(requirement, context=context)
        assert 'FOO' not in context.environ

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  - FOO
"""}, check_env_var_provider)


def test_env_var_provider_with_default_value_in_project_file():
    def check_env_var_provider(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO")
        assert dict(default='from_default') == requirement.options
        local_state_file = LocalStateFile.load_for_directory(dirname)
        status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides())
        context = ProvideContext(environ=dict(),
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)
        result = provider.provide(requirement, context=context)
        assert [] == result.errors
        assert 'FOO' in context.environ
        assert 'from_default' == context.environ['FOO']

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  FOO:
    default: from_default
"""}, check_env_var_provider)


def test_env_var_provider_with_unencrypted_default_value_in_project_file_for_encrypted_requirement():
    # the idea here is that if you want to put an unencrypted
    # password in the file, we aren't going to be annoying and
    # stop you.
    def check_env_var_provider(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO_SECRET")
        assert requirement.encrypted
        assert dict(default='from_default') == requirement.options
        local_state_file = LocalStateFile.load_for_directory(dirname)
        environ = dict()
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        context = ProvideContext(environ=environ,
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)
        result = provider.provide(requirement, context=context)
        assert [] == result.errors
        assert 'FOO_SECRET' in context.environ
        assert 'from_default' == context.environ['FOO_SECRET']

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  FOO_SECRET:
    default: from_default
"""}, check_env_var_provider)


def test_env_var_provider_with_value_set_in_environment():
    def check_env_var_provider(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO")
        local_state_file = LocalStateFile.load_for_directory(dirname)
        environ = dict(FOO='from_environ')
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        assert dict(source='environ', value='from_environ') == status.analysis.config
        context = ProvideContext(environ=environ,
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)
        result = provider.provide(requirement, context=context)
        assert [] == result.errors
        assert 'FOO' in context.environ
        assert 'from_environ' == context.environ['FOO']

    # set a default to be sure we prefer 'environ' instead
    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  FOO:
    default: from_default
"""}, check_env_var_provider)


def test_env_var_provider_with_value_set_in_local_state():
    def check_env_var_provider(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO")
        local_state_file = LocalStateFile.load_for_directory(dirname)
        # set in environ to be sure we override it with local state
        environ = dict(FOO='from_environ')
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        assert dict(value="from_local_state", source="variables") == status.analysis.config
        context = ProvideContext(environ=environ,
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)
        result = provider.provide(requirement, context=context)
        assert [] == result.errors
        assert 'FOO' in context.environ
        assert 'from_local_state' == context.environ['FOO']

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  FOO:
    default: from_default
    """,
         DEFAULT_LOCAL_STATE_FILENAME: """
variables:
  FOO: from_local_state
"""}, check_env_var_provider)


def test_env_var_provider_with_encrypted_value_set_in_local_state():
    def check_env_var_provider(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO_PASSWORD")
        assert requirement.encrypted
        local_state_file = LocalStateFile.load_for_directory(dirname)
        # set in environ to be sure we override it with local state
        environ = dict(FOO_PASSWORD='from_environ')
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        assert dict(value="from_local_state", source="variables") == status.analysis.config
        context = ProvideContext(environ=environ,
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)
        result = provider.provide(requirement, context=context)
        assert [] == result.errors
        assert 'FOO_PASSWORD' in context.environ
        assert 'from_local_state' == context.environ['FOO_PASSWORD']

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  FOO_PASSWORD:
    default: from_default
    """,
         DEFAULT_LOCAL_STATE_FILENAME: """
variables:
  FOO_PASSWORD: from_local_state
"""}, check_env_var_provider)


def test_env_var_provider_configure_local_state_value():
    def check_env_var_provider_config_local_state(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO")
        local_state_file = LocalStateFile.load_for_directory(dirname)
        status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides())
        assert dict(source='unset') == status.analysis.config

        assert local_state_file.get_value(['variables', 'FOO']) is None

        environ = dict()

        provider.set_config_values_as_strings(requirement,
                                              environ,
                                              local_state_file,
                                              'default',
                                              UserConfigOverrides(),
                                              dict(value="bar"))

        assert local_state_file.get_value(['variables', 'FOO']) == "bar"
        local_state_file.save()

        local_state_file_2 = LocalStateFile.load_for_directory(dirname)
        assert local_state_file_2.get_value(['variables', 'FOO']) == "bar"

        # setting empty string = unset
        provider.set_config_values_as_strings(requirement,
                                              environ,
                                              local_state_file,
                                              'default',
                                              UserConfigOverrides(),
                                              dict(value=""))
        assert local_state_file.get_value(['variables', 'FOO']) is None

        local_state_file.save()

        local_state_file_3 = LocalStateFile.load_for_directory(dirname)
        assert local_state_file_3.get_value(['variables', 'FOO']) is None

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  - FOO
"""}, check_env_var_provider_config_local_state)


def test_env_var_provider_configure_encrypted_value():
    def check_env_var_provider_config_local_state(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO_PASSWORD")
        assert requirement.encrypted
        local_state_file = LocalStateFile.load_for_directory(dirname)
        status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides())
        assert dict(source='unset') == status.analysis.config

        assert local_state_file.get_value(['variables', 'FOO_PASSWORD']) is None
        assert set(keyring.fallback_data().values()) == set()

        environ = dict(CONDA_DEFAULT_ENV='/pretend/env', CONDA_ENV_PATH='/pretend/env', CONDA_PREFIX='/pretend/env')

        provider.set_config_values_as_strings(requirement,
                                              environ,
                                              local_state_file,
                                              'default',
                                              UserConfigOverrides(),
                                              dict(value="bar"))

        # should not have affected local state, should use keyring
        assert local_state_file.get_value(['variables', 'FOO_PASSWORD']) is None
        assert set(keyring.fallback_data().values()) == set(["bar"])

        # setting empty string = unset
        provider.set_config_values_as_strings(requirement,
                                              environ,
                                              local_state_file,
                                              'default',
                                              UserConfigOverrides(),
                                              dict(value=""))
        assert local_state_file.get_value(['variables', 'FOO_PASSWORD']) is None
        assert set(keyring.fallback_data().values()) == set()

    keyring.enable_fallback_keyring()
    try:
        with_directory_contents_completing_project_file(
            {DEFAULT_PROJECT_FILENAME: """
variables:
  - FOO_PASSWORD
"""}, check_env_var_provider_config_local_state)
    finally:
        keyring.disable_fallback_keyring()


def test_env_var_provider_configure_disabled_local_state_value():
    def check_env_var_provider_config_disabled_local_state(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO")
        local_state_file = LocalStateFile.load_for_directory(dirname)
        status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides())
        assert dict(source='unset') == status.analysis.config

        assert local_state_file.get_value(['variables', 'FOO']) is None
        assert local_state_file.get_value(['disabled_variables', 'FOO']) is None

        environ = dict()

        # source=environ should mean we set disabled_variables instead of variables
        provider.set_config_values_as_strings(requirement,
                                              environ,
                                              local_state_file,
                                              'default',
                                              UserConfigOverrides(),
                                              dict(source='environ',
                                                   value="bar"))

        assert local_state_file.get_value(['variables', 'FOO']) is None
        assert local_state_file.get_value(['disabled_variables', 'FOO']) == "bar"

        config = provider.read_config(requirement, environ, local_state_file, 'default', UserConfigOverrides())
        assert config == dict(source='unset', value='bar')

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  - FOO
"""}, check_env_var_provider_config_disabled_local_state)


def test_env_var_provider_config_html():
    def check_env_var_provider_config(dirname):
        provider = EnvVarProvider()
        requirement = _load_env_var_requirement(dirname, "FOO")
        local_state_file = LocalStateFile.load_for_directory(dirname)
        environ = dict()
        config = provider.read_config(requirement, environ, local_state_file, 'default', UserConfigOverrides())
        assert dict(source='unset') == config

        # config html when variable is unset
        status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides())
        html = provider.config_html(requirement, environ, local_state_file, UserConfigOverrides(), status)
        assert "Keep" not in html
        assert 'Use this value:' in html

        # config html when variable is unset and we have a default
        requirement.options['default'] = 'from_default'
        status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides())
        html = provider.config_html(requirement, environ, local_state_file, UserConfigOverrides(), status)
        assert "Keep default 'from_default'" in html
        assert 'Use this value instead:' in html

        # config html when variable is set
        environ = dict(FOO='from_environ')
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        html = provider.config_html(requirement, environ, local_state_file, UserConfigOverrides(), status)
        assert "Keep value 'from_environ'" in html
        assert 'Use this value instead:' in html

        # config html when local state override is present
        environ = dict(FOO='from_environ')
        local_state_file.set_value(['variables', 'FOO'], 'from_local_state')
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        html = provider.config_html(requirement, environ, local_state_file, UserConfigOverrides(), status)
        assert "Keep value 'from_environ'" in html
        assert 'Use this value instead:' in html

    # set a default to be sure we prefer 'environ' instead
    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  - FOO
"""}, check_env_var_provider_config)


def test_env_var_provider_prepare_unprepare():
    def check_env_var_provider_prepare(dirname):
        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project, environ=minimal_environ(FOO='bar'))
        assert result
        status = unprepare(project, result)
        assert status
        assert status.status_description == 'Success.'
        assert status.logs == ["Nothing to clean up for FOO.",
                               ("Current environment is not in %s, no need to delete it." % dirname)]

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
variables:
  FOO: null
"""}, check_env_var_provider_prepare)


def test_provide_context_properties():
    def check_provide_contents(dirname):
        environ = dict(foo='bar')
        local_state_file = LocalStateFile.load_for_directory(dirname)
        requirement = EnvVarRequirement(PluginRegistry(), env_var="FOO")
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        context = ProvideContext(environ=environ,
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)
        assert dict(foo='bar') == context.environ
        assert context.status is status

    with_directory_contents(dict(), check_provide_contents)


def test_provide_result_properties():
    empty = ProvideResult.empty()
    assert [] == empty.errors
    assert [] == empty.logs

    full = ProvideResult(['a', 'b'], ['c', 'd'])
    assert ['a', 'b'] == full.errors
    assert ['c', 'd'] == full.logs

    unchanged = full.copy_with_additions()
    assert ['a', 'b'] == unchanged.errors
    assert ['c', 'd'] == unchanged.logs

    unchanged2 = full.copy_with_additions([], [])
    assert ['a', 'b'] == unchanged2.errors
    assert ['c', 'd'] == unchanged2.logs

    extended = full.copy_with_additions(['y'], ['z'])
    assert ['a', 'b', 'y'] == extended.errors
    assert ['c', 'd', 'z'] == extended.logs


def test_provide_context_ensure_service_directory():
    def check_provide_contents(dirname):
        environ = dict()
        local_state_file = LocalStateFile.load_for_directory(dirname)
        requirement = EnvVarRequirement(PluginRegistry(), env_var="FOO")
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        context = ProvideContext(environ=environ,
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)
        workpath = context.ensure_service_directory("foo")
        assert os.path.isdir(workpath)
        assert workpath.endswith("foo")
        parent = os.path.dirname(workpath)
        assert parent.endswith("services")
        parent = os.path.dirname(parent)
        assert parent == dirname

        # be sure we can create if it already exists
        workpath2 = context.ensure_service_directory("foo")
        assert os.path.isdir(workpath2)
        assert workpath == workpath2

    with_directory_contents(dict(), check_provide_contents)


def test_provide_context_ensure_service_directory_cannot_create(monkeypatch):
    def mock_makedirs(path, mode=0):
        raise IOError("this is not EEXIST")

    monkeypatch.setattr("os.makedirs", mock_makedirs)

    def check_provide_contents(dirname):
        environ = dict()
        local_state_file = LocalStateFile.load_for_directory(dirname)
        requirement = EnvVarRequirement(PluginRegistry(), env_var="FOO")
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        context = ProvideContext(environ=environ,
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)
        with pytest.raises(IOError) as excinfo:
            context.ensure_service_directory("foo")
        assert "this is not EEXIST" in repr(excinfo.value)

    with_directory_contents(dict(), check_provide_contents)


def test_provide_context_transform_service_run_state():
    def check_provide_contents(dirname):
        environ = dict()
        local_state_file = LocalStateFile.load_for_directory(dirname)
        local_state_file.set_service_run_state("myservice", dict(port=42))
        requirement = EnvVarRequirement(PluginRegistry(), env_var="FOO")
        status = requirement.check_status(environ, local_state_file, 'default', UserConfigOverrides())
        context = ProvideContext(environ=environ,
                                 local_state_file=local_state_file,
                                 default_env_spec_name='default',
                                 status=status,
                                 mode=PROVIDE_MODE_DEVELOPMENT)

        def transform_it(state):
            assert 42 == state['port']
            state['port'] = 43
            state['foo'] = 'bar'
            return 1234

        result = context.transform_service_run_state("myservice", transform_it)
        assert 1234 == result
        assert dict(port=43, foo='bar') == local_state_file.get_service_run_state("myservice")

    with_directory_contents(dict(), check_provide_contents)


def test_shutdown_service_run_state_nothing_to_do():
    def check(dirname):
        local_state_file = LocalStateFile.load_for_directory(dirname)
        status = shutdown_service_run_state(local_state_file, 'foo')
        assert status
        assert status.status_description == 'Nothing to do to shut down foo.'

    with_directory_contents(dict(), check)


def test_shutdown_service_run_state_command_success():
    def check(dirname):
        local_state_file = LocalStateFile.load_for_directory(dirname)
        true_commandline = tmp_script_commandline("""import sys
sys.exit(0)
""")
        local_state_file.set_service_run_state('FOO', {'shutdown_commands': [true_commandline]})
        status = shutdown_service_run_state(local_state_file, 'FOO')
        assert status
        assert status.status_description == "Successfully shut down FOO."

    with_directory_contents(dict(), check)


def test_shutdown_service_run_state_command_failure():
    def check(dirname):
        local_state_file = LocalStateFile.load_for_directory(dirname)
        false_commandline = tmp_script_commandline("""import sys
sys.exit(1)
""")
        local_state_file.set_service_run_state('FOO', {'shutdown_commands': [false_commandline]})
        status = shutdown_service_run_state(local_state_file, 'FOO')
        assert not status
        assert status.status_description == "Shutdown commands failed for FOO."
        assert status.errors == ["Shutting down FOO, command %r failed with code 1." % false_commandline]

    with_directory_contents(dict(), check)
