# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2016, Anaconda, Inc. All rights reserved.
#
# Licensed under the terms of the BSD 3-Clause License.
# The full license is in the file LICENSE.txt, distributed with this software.
# -----------------------------------------------------------------------------
from __future__ import absolute_import, print_function

from copy import deepcopy
from distutils.spawn import find_executable
import os
import platform
import stat
import subprocess
import sys
import time

import pytest

from anaconda_project.env_spec import _load_environment_yml
from anaconda_project.version import version
from anaconda_project.internal.test.tmpfile_utils import (with_directory_contents,
                                                          with_directory_contents_completing_project_file)
from anaconda_project.internal import conda_api
from anaconda_project.requirements_registry.registry import RequirementsRegistry
from anaconda_project.requirements_registry.requirement import EnvVarRequirement
from anaconda_project.requirements_registry.requirements.conda_env import CondaEnvRequirement
from anaconda_project.requirements_registry.requirements.service import ServiceRequirement
from anaconda_project.requirements_registry.requirements.download import DownloadRequirement
from anaconda_project.project import (Project, ProjectProblem)
from anaconda_project.project_file import DEFAULT_PROJECT_FILENAME
from anaconda_project.project_lock_file import DEFAULT_PROJECT_LOCK_FILENAME
from anaconda_project.test.environ_utils import minimal_environ
from anaconda_project.test.project_utils import project_no_dedicated_env


def _assert_find_executable(name, path):
    found = find_executable(name, path)
    if found is None:
        print("Did not find %s on path %r, running the tests requires it" % (name, path))
    assert found is not None
    return found


def test_properties():
    def check_properties(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.problems == []
        assert dirname == project.directory_path
        assert dirname == os.path.dirname(project.project_file.filename)
        assert project.name == os.path.basename(dirname)
        assert project.url_friendly_name == os.path.basename(dirname)
        assert project.description == ''

    with_directory_contents(dict(), check_properties)


def test_ignore_trailing_slash_on_dirname():
    def check_properties(dirname):
        project = project_no_dedicated_env(dirname + "/")
        assert project.problems == []
        assert dirname == project.directory_path
        assert dirname == os.path.dirname(project.project_file.filename)
        assert project.name == os.path.basename(dirname)

    with_directory_contents(dict(), check_properties)


def test_single_env_var_requirement():
    def check_some_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 2 == len(requirements)
        assert "FOO" == requirements[0].env_var
        assert dict() == requirements[0].options

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[1].env_var

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
variables:
  FOO: {}
"""
    }, check_some_env_var)


def test_single_env_var_requirement_with_description():
    def check_some_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 2 == len(requirements)
        assert "FOO" == requirements[0].env_var
        assert {'description': "Set FOO to the value of your foo"} == requirements[0].options
        assert "Set FOO to the value of your foo" == requirements[0].description
        assert "FOO" == requirements[0].title

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[1].env_var

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
variables:
  FOO: { description: "Set FOO to the value of your foo" }
"""
        }, check_some_env_var)


def test_single_env_var_requirement_null_for_default():
    def check_some_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 3 == len(requirements)
        assert "FOO" == requirements[0].env_var
        assert dict() == requirements[0].options
        assert "BAR" == requirements[1].env_var
        assert dict() == requirements[1].options

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[2].env_var

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
variables:
  FOO: null
  BAR: { default: null }
"""
        }, check_some_env_var)


def test_single_env_var_requirement_string_for_default():
    def check_some_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 2 == len(requirements)
        assert "FOO" == requirements[0].env_var
        assert dict(default='hello') == requirements[0].options

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[1].env_var

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
variables:
  FOO: 'hello'
"""
    }, check_some_env_var)


def test_single_env_var_requirement_number_for_default():
    def check_some_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 2 == len(requirements)
        assert "FOO" == requirements[0].env_var
        assert dict(default='42') == requirements[0].options

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[1].env_var

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
variables:
  FOO: 42
"""
    }, check_some_env_var)


def test_single_env_var_requirement_default_is_in_dict():
    def check_some_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 2 == len(requirements)
        assert "FOO" == requirements[0].env_var
        assert dict(default='42') == requirements[0].options

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[1].env_var

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
variables:
  FOO: { default: 42 }
"""
    }, check_some_env_var)


def test_requirement_inheritance():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        assert project.default_env_spec_name == 'foo'

        def var_reqs(name):
            conda_env_reqs = [req for req in project.requirements(name) if isinstance(req, CondaEnvRequirement)]
            assert 1 == len(conda_env_reqs)
            requirements = [(req.env_var, req.options.get('default')) for req in project.requirements(name)
                            if not isinstance(req, CondaEnvRequirement)]
            requirements = sorted(requirements, key=lambda x: x[0])
            return ([t[0] for t in requirements], [t[1] for t in requirements])

        requirements = var_reqs('foo')
        assert (['BAR', 'DOWNLOAD', 'FOO'], ['hi', None, 'global']) == requirements

        requirements = var_reqs('foo_with_override')
        assert (['DOWNLOAD', 'FOO'], [None, 'local']) == requirements

        requirements = var_reqs('bar')
        assert (['DOWNLOAD', 'FOO'], [None, 'global']) == requirements

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
variables:
  FOO: { default: "global" }
downloads:
  DOWNLOAD: "http://example.com"
env_specs:
  foo:
    variables:
      BAR: "hi"
  foo_with_override:
    variables:
      FOO: "local"
  bar:
    downloads:
      DOWNLOAD: "http://example.com/bar"
"""
    }, check)


def test_problem_in_project_file():
    def check_problem(dirname):
        project = project_no_dedicated_env(dirname)
        requirements = project.requirements(project.default_env_spec_name)
        assert 0 == len(requirements)
        assert 1 == len(project.problems)

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: """
variables:
  42
"""}, check_problem)


def test_problem_empty_names():
    def check_problem(dirname):
        project = project_no_dedicated_env(dirname)

        def _fn(s):
            return "%s: %s" % (project.project_file.basename, s)

        assert _fn("Variable name cannot be empty string, found: ' ' as name") in project.problems
        assert _fn("Download name cannot be empty string, found: ' ' as name") in project.problems
        assert _fn("Service name cannot be empty string, found: ' ' as name") in project.problems
        assert _fn("Environment spec name cannot be empty string, found: ' ' as name") in project.problems
        assert _fn("Command variable name cannot be empty string, found: ' ' as name") in project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
variables:
  ' ': 'thing'
downloads:
  ' ': 'http://localhost:8000/foo.tgz'
services:
  ' ': redis
env_specs:
  ' ':
    packages:
       - python
commands:
  ' ':
    shell: echo 'foo'
"""
    }, check_problem)


def test_problem_empty_names_var_list():
    def check_problem(dirname):
        project = project_no_dedicated_env(dirname)
        assert ("%s: Variable name cannot be empty string, found: ' ' as name" %
                project.project_file.basename) in project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
variables:
  - ' '
"""
    }, check_problem)


def test_project_dir_does_not_exist():
    def check_does_not_exist(dirname):
        project_dir = os.path.join(dirname, 'foo')
        assert not os.path.isdir(project_dir)
        project = Project(project_dir)
        assert not os.path.isdir(project_dir)
        assert ["Project directory '%s' does not exist." % project_dir] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 0 == len(requirements)

    with_directory_contents(dict(), check_does_not_exist)


def test_project_dir_not_readable(monkeypatch):
    def check_not_readable(dirname):
        project_dir = os.path.join(dirname, 'foo')
        os.makedirs(project_dir)

        def mock_os_walk(dirname):
            raise OSError("NOPE")

        monkeypatch.setattr('os.walk', mock_os_walk)

        project = Project(project_dir)

        assert ["Could not list files in %s: NOPE." % project_dir] == project.problems

    with_directory_contents(dict(), check_not_readable)


def test_single_env_var_requirement_with_options():
    def check_some_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        requirements = project.requirements(project.default_env_spec_name)
        assert 2 == len(requirements)
        assert "FOO" == requirements[0].env_var
        assert dict(default="hello") == requirements[0].options

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[1].env_var

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
variables:
    FOO: { default: "hello" }
"""
        }, check_some_env_var)


def test_override_plugin_registry():
    def check_override_plugin_registry(dirname):
        registry = RequirementsRegistry()
        project = project_no_dedicated_env(dirname, registry)
        assert project._config_cache.registry is registry

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
variables:
  FOO: {}
"""
    }, check_override_plugin_registry)


def test_get_name_from_project_file():
    def check_name_from_project_file(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.name == "foo"

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
name: foo
    """
    }, check_name_from_project_file)


def test_broken_name_in_project_file():
    def check_name_from_project_file(dirname):
        project = project_no_dedicated_env(dirname)
        assert [(DEFAULT_PROJECT_FILENAME + ": name: field should have a string value not []")] == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
name: []
    """
    }, check_name_from_project_file)


def test_get_name_from_directory_name():
    def check_name_from_directory_name(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.name == os.path.basename(dirname)

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
name: null
"""
    }, check_name_from_directory_name)


def test_set_name_in_project_file():
    def check_set_name(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.name == "foo"

        project.project_file.set_value('name', "bar")
        assert project.name == "foo"
        project.project_file.save()
        assert project.name == "bar"

        project2 = project_no_dedicated_env(dirname)
        assert project2.name == "bar"

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: """
name: foo
"""}, check_set_name)


def test_get_description_from_project_file():
    def check_description_from_project_file(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.description == "foo"

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
description: foo
    """
    }, check_description_from_project_file)


def test_broken_description_in_project_file():
    def check_description_from_project_file(dirname):
        project = project_no_dedicated_env(dirname)
        assert [(DEFAULT_PROJECT_FILENAME + ": description: field should have a string value not []")
                ] == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
description: []
    """
    }, check_description_from_project_file)


def test_get_icon_from_project_file():
    def check_icon_from_project_file(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.icon == os.path.join(dirname, "foo.png")

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
icon: foo.png
        """,
        "foo.png": ""
    }, check_icon_from_project_file)


def test_broken_icon_in_project_file():
    def check_icon_from_project_file(dirname):
        project = project_no_dedicated_env(dirname)
        assert [(DEFAULT_PROJECT_FILENAME + ": icon: field should have a string value not []")] == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
icon: []
    """
    }, check_icon_from_project_file)


def test_nonexistent_icon_in_project_file():
    def check_icon_from_project_file(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.icon is None
        assert ["Icon file %s does not exist." % (os.path.join(dirname, "foo.png"))] == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
icon: foo.png
    """
    }, check_icon_from_project_file)


def test_set_icon_in_project_file():
    def check_set_icon(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.icon == os.path.join(dirname, "foo.png")

        project.project_file.set_value('icon', "bar.png")
        assert project.icon == os.path.join(dirname, "foo.png")
        project.project_file.save()
        assert project.icon == os.path.join(dirname, "bar.png")

        project2 = project_no_dedicated_env(dirname)
        assert project2.icon == os.path.join(dirname, "bar.png")

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
icon: foo.png
""",
        "foo.png": "",
        "bar.png": ""
    }, check_set_icon)


def test_get_package_requirements_from_project_file():
    def check_get_packages(dirname):
        project = project_no_dedicated_env(dirname)
        env = project.env_specs['default']
        assert env.name == 'default'
        assert ("mtv", "hbo") == env.channels
        assert ("foo", "hello >= 1.0", "world") == env.conda_packages
        assert ("pip1", "pip2==1.3", "pip3") == env.pip_packages
        assert set(["foo", "hello", "world"]) == env.conda_package_names_set
        assert set(["pip1", "pip2", "pip3"]) == env.pip_package_names_set

        requirements = project.requirements(project.default_env_spec_name)

        # find CondaEnvRequirement
        conda_env_req = None
        for r in requirements:
            if isinstance(r, CondaEnvRequirement):
                assert conda_env_req is None  # only one
                conda_env_req = r
        assert len(conda_env_req.env_specs) == 1
        assert 'default' in conda_env_req.env_specs
        assert conda_env_req.env_specs['default'] is env

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
packages:
  - foo
  - hello >= 1.0
  - world
  - pip:
     - pip1
     - pip2==1.3
  - pip:
     - pip3

channels:
  - mtv
  - hbo
    """
    }, check_get_packages)


def test_get_package_requirements_from_empty_project():
    def check_get_packages(dirname):
        project = project_no_dedicated_env(dirname)
        assert () == project.env_specs['default'].conda_packages

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: ""}, check_get_packages)


def test_complain_about_packages_not_a_list():
    def check_get_packages(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        "should be a list of strings not" in project.problems[0]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
packages:
    foo: bar
    """
    }, check_get_packages)


def test_complain_about_pip_deps_not_a_list():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        "should be a list of strings not" in project.problems[0]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
packages:
    - pip: bar
    """
    }, check)


def test_complain_about_pip_deps_not_a_string():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        "should be a list of pip package names" in project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
packages:
    - pip:
      - {}
    """
        }, check)


def test_complain_about_packages_bad_spec():
    def check_get_packages(dirname):
        project = project_no_dedicated_env(dirname)
        filename = project.project_file.basename
        assert [
            "%s: invalid package specification: =" % filename,
            "%s: invalid package specification: foo bar" % filename,
            "%s: invalid pip package specifier: %%" % filename
        ] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
packages:
    - "="
    - foo bar
    - pip:
      - "%"
    """
        }, check_get_packages)


def test_complain_about_conda_env_in_variables_list():
    def check_complain_about_conda_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        template = (project.project_file.basename + ": Environment variable %s is reserved for Conda's use, " +
                    "so it can't appear in the variables section.")
        assert [template % 'CONDA_ENV_PATH', template % 'CONDA_DEFAULT_ENV',
                template % 'CONDA_PREFIX'] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
variables:
  - CONDA_ENV_PATH
  - CONDA_DEFAULT_ENV
  - CONDA_PREFIX
    """
        }, check_complain_about_conda_env_var)


def test_complain_about_conda_env_in_variables_dict():
    def check_complain_about_conda_env_var(dirname):
        project = project_no_dedicated_env(dirname)
        template = (project.project_file.basename + ": Environment variable %s is reserved for Conda's use, " +
                    "so it can't appear in the variables section.")
        assert [template % 'CONDA_ENV_PATH', template % 'CONDA_DEFAULT_ENV',
                template % 'CONDA_PREFIX'] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
variables:
  CONDA_ENV_PATH: {}
  CONDA_DEFAULT_ENV: {}
  CONDA_PREFIX: {}
    """
        }, check_complain_about_conda_env_var)


def test_load_environments():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert 0 == len(project.problems)
        assert len(project.env_specs) == 5
        assert 'foo' in project.env_specs
        assert 'bar' in project.env_specs
        assert 'foo_child' in project.env_specs
        assert 'foo_grandchild' in project.env_specs
        assert 'mixin' in project.env_specs
        assert project.default_env_spec_name == 'foo'
        foo = project.env_specs['foo']
        bar = project.env_specs['bar']
        foo_child = project.env_specs['foo_child']
        foo_grandchild = project.env_specs['foo_grandchild']
        mixin = project.env_specs['mixin']
        assert foo.conda_packages == ('global1=1.0', 'global2=1.0', 'python', 'dog', 'cat', 'zebra')
        assert foo.description == "THE FOO"
        assert foo.pip_packages == ()
        assert foo.channels == ('univision', )
        assert foo.inherit_from == (project.global_base_env_spec, )
        assert bar.conda_packages == ('global1=1.0', 'global2=1.0')
        assert bar.description == "bar"
        assert bar.pip_packages == ()
        assert bar.channels == ('univision', )
        assert bar.inherit_from == (project.global_base_env_spec, )

        assert mixin.conda_packages == ('global1=1.0', 'bunny', 'walrus=1.0', 'global2=2.0')
        assert mixin.pip_packages == ('bear', )
        assert mixin.channels == (
            'univision',
            'hbo',
        )

        assert foo_child.description == 'foo_child'
        assert foo_child.conda_packages == ('global2=1.0', 'python', 'cat', 'zebra', 'dog=2.0', 'global1=2.0', 'lion')
        assert foo_child.pip_packages == ('fish', )
        assert foo_child.channels == (
            'univision',
            'abc',
        )
        assert foo_child.inherit_from == (foo, )

        assert foo_grandchild.description == 'foo_grandchild'
        # the resulting order here is important, and reflects that we linearized
        # the inheritance hierarchy
        assert foo_grandchild.conda_packages == ('python', 'cat', 'zebra', 'global1=2.0', 'lion', 'bunny',
                                                 'global2=2.0', 'walrus=2.0', 'dog=3.0')
        assert foo_grandchild.pip_packages == ('fish', 'bear', 'seahorse')
        assert foo_grandchild.channels == ('univision', 'abc', 'hbo', 'nbc')
        assert foo_grandchild.inherit_from == (foo_child, mixin)

        # While we have this complicated inheritance tree around, be
        # sure we can round-trip save it to environment.yml files
        for spec in (foo, bar, mixin, foo_child, foo_grandchild):
            saved = os.path.join(dirname, "saved-%s" % spec.name)
            spec.save_environment_yml(saved)
            loaded = _load_environment_yml(saved)

            assert loaded.inherit_from == ()
            assert loaded.inherit_from_names == ()
            assert loaded.name == spec.name
            assert loaded.conda_packages == spec.conda_packages
            assert loaded.pip_packages == spec.pip_packages
            assert loaded.channels == spec.channels

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
packages:
  - global1=1.0
  - global2=1.0
channels:
  - univision

env_specs:
  foo:
    description: "THE FOO"
    packages:
       - python
       - dog
       - cat
       - zebra
  bar: {}
  foo_child:
    inherit_from: foo
    packages:
       - dog=2.0
       - global1=2.0
       - lion
       - pip:
          - fish
    channels:
       - abc
  mixin:
    packages:
       - bunny
       - walrus=1.0
       - global2=2.0
       - pip:
         - bear
    channels:
       - hbo
  foo_grandchild:
    inherit_from: [foo_child, mixin]
    packages:
       - walrus=2.0
       - dog=3.0
       - pip:
         - seahorse
    channels:
       - nbc
    """
    }, check_environments)


def test_load_environments_merging_in_global():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert 0 == len(project.problems)
        assert len(project.env_specs) == 3
        assert 'default' in project.env_specs
        assert 'foo' in project.env_specs
        assert 'bar' in project.env_specs
        assert project.default_env_spec_name == 'default'
        default = project.env_specs['default']
        foo = project.env_specs['foo']
        bar = project.env_specs['bar']
        assert default.conda_packages == ('dead-parrot', 'elephant', 'lion')
        assert foo.conda_packages == ('dead-parrot', 'elephant', 'python', 'dog', 'cat', 'zebra')
        assert bar.conda_packages == ('dead-parrot', 'elephant')
        assert default.channels == ('mtv', 'cartoons')
        assert foo.channels == ('mtv', 'hbo')
        assert bar.channels == ('mtv', )

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
packages:
  - dead-parrot
  - elephant

channels:
  - mtv

env_specs:
  foo:
    packages:
       - python
       - dog
       - cat
       - zebra
    channels:
       - hbo
  bar: {}
  default:
    packages:
      - lion
    channels:
      - cartoons
    """
    }, check_environments)


def test_load_environments_default_always_default_even_if_not_first():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert 0 == len(project.problems)
        assert len(project.env_specs) == 3
        assert 'foo' in project.env_specs
        assert 'bar' in project.env_specs
        assert 'default' in project.env_specs
        assert project.default_env_spec_name == 'default'
        foo = project.env_specs['foo']
        bar = project.env_specs['bar']
        default = project.env_specs['default']
        assert foo.conda_packages == ()
        assert bar.conda_packages == ()
        assert default.conda_packages == ()

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
env_specs:
  foo: {}
  bar: {}
  default: {}
    """
        }, check_environments)


def test_complain_about_environments_not_a_dict():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        "should be a directory from environment name to environment attributes" in project.problems[0]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
env_specs: 42
    """
    }, check_environments)


def test_complain_about_non_string_environment_description():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert ["%s: 'description' field of environment foo must be a string" % (project.project_file.basename)
                ] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
env_specs:
   foo:
     description: []
    """
        }, check_environments)


def test_complain_about_non_string_inherit_from():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        assert "inherit_from: value should be a list of env spec names" in project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
env_specs:
   foo:
     inherit_from: {}
    """
        }, check_environments)


def test_complain_about_packages_list_of_wrong_thing():
    def check_get_packages(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        "should be a string not '42'" in project.problems[0]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: """
packages:
    - 42
    """
    }, check_get_packages)


def test_complain_about_env_spec_inherits_from_nonexistent():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert [
            "%s: name 'bar' in 'inherit_from' field of env spec foo does not match the name of another env spec" %
            (project.project_file.basename)
        ] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
env_specs:
   foo:
      inherit_from: bar
    """
        }, check_environments)


def test_complain_about_cycle_of_two_env_specs():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert [
            "%s: 'inherit_from' fields create circular inheritance among these env specs: bar, foo" %
            (project.project_file.basename)
        ] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
env_specs:
   foo:
      inherit_from: bar
   bar:
      inherit_from: foo
    """
        }, check_environments)


def test_complain_about_cycle_of_many_env_specs():
    def check_environments(dirname):
        project = project_no_dedicated_env(dirname)
        assert [
            "%s: 'inherit_from' fields create circular inheritance among these env specs: a, b, c, d, e" %
            (project.project_file.basename)
        ] == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
env_specs:
   a:
      inherit_from: b
   b:
      inherit_from: c
   c:
      inherit_from: d
   d:
      inherit_from: e
   e:
      inherit_from: a

    """
    }, check_environments)


def test_load_list_of_variables_requirements():
    def check_file(dirname):
        filename = os.path.join(dirname, DEFAULT_PROJECT_FILENAME)
        assert os.path.exists(filename)
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 3 == len(requirements)
        assert isinstance(requirements[0], EnvVarRequirement)
        assert 'FOO' == requirements[0].env_var
        assert isinstance(requirements[1], EnvVarRequirement)
        assert 'BAR' == requirements[1].env_var
        assert isinstance(requirements[2], CondaEnvRequirement)

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[2].env_var

        assert dict() == requirements[2].options
        assert len(project.problems) == 0

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: "variables:\n  - FOO\n  - BAR\n"
    }, check_file)


def test_load_dict_of_variables_requirements():
    def check_file(dirname):
        filename = os.path.join(dirname, DEFAULT_PROJECT_FILENAME)
        assert os.path.exists(filename)
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        requirements = project.requirements(project.default_env_spec_name)
        assert 3 == len(requirements)
        assert isinstance(requirements[0], EnvVarRequirement)
        assert 'FOO' == requirements[0].env_var
        assert dict(a=1) == requirements[0].options
        assert isinstance(requirements[1], EnvVarRequirement)
        assert 'BAR' == requirements[1].env_var
        assert dict(b=2) == requirements[1].options
        assert isinstance(requirements[2], CondaEnvRequirement)

        conda_env_var = conda_api.conda_prefix_variable()
        assert conda_env_var == requirements[2].env_var

        assert dict() == requirements[2].options
        assert len(project.problems) == 0

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "variables:\n  FOO: { a: 1 }\n  BAR: { b: 2 }\n"
        }, check_file)


def test_non_string_variables_requirements():
    def check_file(dirname):
        filename = os.path.join(dirname, DEFAULT_PROJECT_FILENAME)
        assert os.path.exists(filename)
        project = project_no_dedicated_env(dirname)
        assert 2 == len(project.problems)
        requirements = project.requirements(project.default_env_spec_name)
        assert 0 == len(requirements)
        assert "42 is not a string" in project.problems[0]
        assert "43 is not a string" in project.problems[1]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: "variables:\n  - 42\n  - 43\n"
    }, check_file)


def test_variable_default_cannot_be_bool():
    def check_file(dirname):
        filename = os.path.join(dirname, DEFAULT_PROJECT_FILENAME)
        assert os.path.exists(filename)
        project = project_no_dedicated_env(dirname)
        requirements = project.requirements(project.default_env_spec_name)
        assert [] == requirements
        assert 1 == len(project.problems)
        assert "default value for variable FOO must be null, a string, or a number" in project.problems[0]

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: "variables:\n  FOO: true\n"}, check_file)


def test_variable_default_cannot_be_list():
    def check_file(dirname):
        filename = os.path.join(dirname, DEFAULT_PROJECT_FILENAME)
        assert os.path.exists(filename)
        project = project_no_dedicated_env(dirname)
        requirements = project.requirements(project.default_env_spec_name)
        assert [] == requirements
        assert 1 == len(project.problems)
        assert "default value for variable FOO must be null, a string, or a number" in project.problems[0]

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: "variables:\n  FOO: []\n"}, check_file)


def test_variable_default_missing_key_field():
    def check(dirname):
        filename = os.path.join(dirname, DEFAULT_PROJECT_FILENAME)
        assert os.path.exists(filename)
        project = project_no_dedicated_env(dirname)
        requirements = project.requirements(project.default_env_spec_name)
        assert [] == requirements
        assert 1 == len(project.problems)
        assert "default value for variable FOO must be null, a string, or a number" in project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
variables:
  FOO:
    default: { encrypted: 'abcdefg' }
"""
        }, check)


def test_variables_requirements_not_a_collection():
    def check_file(dirname):
        filename = os.path.join(dirname, DEFAULT_PROJECT_FILENAME)
        assert os.path.exists(filename)
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        requirements = project.requirements(project.default_env_spec_name)
        assert 0 == len(requirements)
        assert "variables section contains wrong value type 42" in project.problems[0]

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: "variables:\n  42\n"}, check_file)


def test_corrupted_project_file():
    def check_problem(dirname):
        project = project_no_dedicated_env(dirname)
        requirements = project.requirements(project.default_env_spec_name)
        assert 0 == len(requirements)
        assert 1 == len(project.problems)
        assert 'anaconda-project.yml: Syntax error: ' in project.problems[0]

        problem1 = project.problem_objects[0]
        assert problem1.maybe_line_number == 2
        assert problem1.maybe_column_number == 9
        assert problem1.maybe_filename == project.project_file.filename
        assert problem1.text_without_filename.startswith("Syntax error:")

    with_directory_contents({DEFAULT_PROJECT_FILENAME: """
^
variables:
  FOO
"""}, check_problem)


def test_corrupted_project_lock_file():
    def check_problem(dirname):
        project = project_no_dedicated_env(dirname)
        requirements = project.requirements(project.default_env_spec_name)
        assert 0 == len(requirements)
        assert 1 == len(project.problems)
        assert 'anaconda-project-lock.yml: Syntax error: ' in project.problems[0]

        problem1 = project.problem_objects[0]
        assert problem1.maybe_line_number == 2
        assert problem1.maybe_column_number == 5
        assert problem1.maybe_filename == project.lock_file.filename
        assert problem1.text_without_filename.startswith("Syntax error:")

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_LOCK_FILENAME: """
^
stuff:
  FOO
"""
    }, check_problem)


def test_non_dict_commands_section():
    def check_app_entry(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: 'commands:' section should be a dictionary from command names to attributes, not %r" % (
            project.project_file.basename, 42)
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: "commands:\n  42\n"}, check_app_entry)


def test_non_dict_services_section():
    def check_app_entry(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = ("%s: 'services:' section should be a dictionary from environment variable " +
                          "to service type, found %r") % (project.project_file.basename, 42)
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: "services:\n  42\n"}, check_app_entry)


def test_non_string_as_value_of_command():
    def check_app_entry(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: command name '%s' should be followed by a dictionary of attributes not %r" % (
            project.project_file.basename, 'default', 42)
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: "commands:\n default: 42\n"
    }, check_app_entry)


def test_empty_command():
    def check_app_entry(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: command '%s' does not have a command line in it" % (project.project_file.basename,
                                                                                  'default')
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: "commands:\n default: {}\n"
    }, check_app_entry)


def test_command_with_bogus_key():
    def check_app_entry(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: command '%s' does not have a command line in it" % (project.project_file.basename,
                                                                                  'default')
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    foobar: 'boo'\n"
        }, check_app_entry)


def test_command_with_non_string_description():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: 'description' field of command %s must be a string" % (project.project_file.basename,
                                                                                     'default')
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n     unix: 'boo'\n     description: []\n"
        }, check)


def test_command_with_non_boolean_supports_http_options():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: 'supports_http_options' field of command %s must be a boolean" % (
            project.project_file.basename, 'default')
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n     unix: 'boo'\n     supports_http_options: 'blah'\n"
        }, check)


def test_command_with_non_boolean_registers_fusion_function():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: 'registers_fusion_function' field of command %s must be a boolean" % (
            project.project_file.basename, 'default')
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n     unix: 'boo'\n     registers_fusion_function: 'blah'\n"
        }, check)


def test_command_with_extras():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems

        assert 'default' in project.commands
        command = project.commands['default']
        assert command.extras == {"registers_fusion_function": True}

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n     unix: 'boo'\n     registers_fusion_function: true\n"
        }, check)


def test_command_with_custom_description():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        command = project.default_command
        assert command.bokeh_app == 'test.py'
        assert command.description == 'hi'

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        "commands:\n default:\n    bokeh_app: test.py\n    description: hi\npackages:\n - bokeh\n"
    }, check)


def test_command_with_non_string_env_spec():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_error = "%s: 'env_spec' field of command %s must be a string (an environment spec name)" % (
            project.project_file.basename, 'default')
        assert [expected_error] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n     unix: 'boo'\n     env_spec: []\n"
        }, check)


def test_command_with_nonexistent_env_spec():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_error = "%s: env_spec 'boo' for command '%s' does not appear in the env_specs section" % (
            project.project_file.basename, 'default')
        assert [expected_error] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n     unix: 'boo'\n     env_spec: boo\n"
        }, check)


def test_command_with_many_problems_at_once():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_errors = [
            "%s: 'description' field of command default must be a string",
            "%s: env_spec 'nonexistent' for command 'default' does not appear in the env_specs section",
            "%s: command 'default' has multiple commands in it, 'notebook' can't go with 'unix'"
        ]
        expected_errors = list(map(lambda e: e % project.project_file.basename, expected_errors))
        assert expected_errors == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
commands:
  default:
     unix: bar
     notebook: foo.ipynb
     env_spec: nonexistent
     description: []
        """
    }, check)


def test_command_with_bogus_key_and_ok_key():
    def check_app_entry(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        command = project.default_command
        assert command.name == 'default'
        assert command.unix_shell_commandline == 'bar'

        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    foobar: 'boo'\n\n    unix: 'bar'\n"
        }, check_app_entry)


def test_two_empty_commands():
    def check_app_entry(dirname):
        project = project_no_dedicated_env(dirname)
        assert 2 == len(project.problems)
        expected_error_1 = "%s: command '%s' does not have a command line in it" % (project.project_file.basename,
                                                                                    'foo')
        expected_error_2 = "%s: command '%s' does not have a command line in it" % (project.project_file.basename,
                                                                                    'bar')
        assert expected_error_1 == project.problems[0]
        assert expected_error_2 == project.problems[1]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: "commands:\n foo: {}\n bar: {}\n"
    }, check_app_entry)


def test_non_string_as_value_of_conda_app_entry():
    def check_app_entry(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: command '%s' attribute '%s' should be a string not '%r'" % (
            project.project_file.basename, 'default', 'conda_app_entry', 42)
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    conda_app_entry: 42\n"
        }, check_app_entry)


def test_non_string_as_value_of_shell():
    def check_shell_non_dict(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: command '%s' attribute '%s' should be a string not '%r'" % (project.project_file.basename,
                                                                                          'default', 'unix', 42)
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    unix: 42\n"
    }, check_shell_non_dict)


def test_notebook_command():
    def check_notebook_command(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.notebook == 'test.ipynb'
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(environ)
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        jupyter_notebook = _assert_find_executable('jupyter-notebook', path)
        assert cmd_exec.args == [
            jupyter_notebook,
            os.path.join(dirname, 'test.ipynb'), '--NotebookApp.default_url=/notebooks/test.ipynb'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    notebook: test.ipynb\n"
        }, check_notebook_command)


def test_notebook_command_extra_args():
    def check_notebook_command_extra_args(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.notebook == 'test.ipynb'
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(environ, extra_args=['foo', 'bar'])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        jupyter_notebook = _assert_find_executable('jupyter-notebook', path)
        assert cmd_exec.args == [
            jupyter_notebook,
            os.path.join(dirname, 'test.ipynb'), '--NotebookApp.default_url=/notebooks/test.ipynb', 'foo', 'bar'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    notebook: test.ipynb\n"
        }, check_notebook_command_extra_args)


def test_notebook_command_with_project_http_args():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.notebook == 'test.ipynb'
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        assert command.supports_http_options

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ,
            extra_args=[
                'foo', 'bar', '--anaconda-project-url-prefix', 'blah', '--anaconda-project-port', '1234',
                '--anaconda-project-host', 'example.com', '--anaconda-project-no-browser',
                '--anaconda-project-iframe-hosts=foo1.com *.foo2.com', '--anaconda-project-use-xheaders',
                '--anaconda-project-address', '1.2.3.4'
            ])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        jupyter_notebook = _assert_find_executable('jupyter-notebook', path)
        assert cmd_exec.args == [
            jupyter_notebook,
            os.path.join(dirname, 'test.ipynb'), '--NotebookApp.default_url=/notebooks/test.ipynb', '--ip', '1.2.3.4',
            '--NotebookApp.tornado_settings=' +
            """{ 'headers': { 'Content-Security-Policy': "frame-ancestors 'self' foo1.com *.foo2.com" } }""",
            '--no-browser', '--port', '1234', '--NotebookApp.base_url=blah', '--NotebookApp.trust_xheaders=True', 'foo',
            'bar'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    notebook: test.ipynb\n"
        }, check)


def test_notebook_command_disabled_project_http_args():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.notebook == 'test.ipynb'
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        assert not command.supports_http_options

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ,
            extra_args=[
                'foo', 'bar', '--anaconda-project-url-prefix', 'blah', '--anaconda-project-port', '1234',
                '--anaconda-project-host', 'example.com', '--anaconda-project-no-browser',
                '--anaconda-project-use-xheaders', '--anaconda-project-address', '1.2.3.4'
            ])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        jupyter_notebook = _assert_find_executable('jupyter-notebook', path)
        assert cmd_exec.args == [jupyter_notebook, os.path.join(dirname, 'test.ipynb'), 'foo', 'bar']
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        "commands:\n default:\n    notebook: test.ipynb\n    supports_http_options: false\n"
    }, check)


def test_notebook_command_project_http_args_after_double_hyphen():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.notebook == 'test.ipynb'
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        assert command.supports_http_options

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ,
            extra_args=[
                '--', 'foo', 'bar', '--anaconda-project-url-prefix', 'blah', '--anaconda-project-port', '1234',
                '--anaconda-project-host', 'example.com', '--anaconda-project-no-browser',
                '--anaconda-project-use-xheaders'
            ])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        jupyter_notebook = _assert_find_executable('jupyter-notebook', path)
        assert cmd_exec.args == [
            jupyter_notebook,
            os.path.join(dirname, 'test.ipynb'), '--NotebookApp.default_url=/notebooks/test.ipynb', '--', 'foo', 'bar',
            '--anaconda-project-url-prefix', 'blah', '--anaconda-project-port', '1234', '--anaconda-project-host',
            'example.com', '--anaconda-project-no-browser', '--anaconda-project-use-xheaders'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    notebook: test.ipynb\n"
        }, check)


def test_notebook_command_with_project_http_args_separated_by_equals():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.notebook == 'test.ipynb'
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ,
            extra_args=[
                'foo', 'bar', '--anaconda-project-url-prefix=blah', '--anaconda-project-port=1234',
                '--anaconda-project-host=example.com', '--anaconda-project-no-browser',
                '--anaconda-project-address=1.2.3.4'
            ])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        jupyter_notebook = _assert_find_executable('jupyter-notebook', path)
        assert cmd_exec.args == [
            jupyter_notebook,
            os.path.join(dirname, 'test.ipynb'), '--NotebookApp.default_url=/notebooks/test.ipynb', '--ip', '1.2.3.4',
            '--no-browser', '--port', '1234', '--NotebookApp.base_url=blah', 'foo', 'bar'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    notebook: test.ipynb\n"
        }, check)


def test_notebook_guess_command():
    def check_notebook_guess_command(dirname):
        project = project_no_dedicated_env(dirname)

        assert ["%s: No command runs notebook test.ipynb" % project.project_file.basename] == project.suggestions

        project.suggestion_objects[0].fix(project)
        project.project_file.save()

        assert 'test.ipynb' in project.commands
        assert 'default' in project.commands
        assert len(project.commands) == 2  # we should have ignored all the ignored ones
        assert project.default_command.name == 'default'

        command = project.commands['test.ipynb']
        assert command.notebook == 'test.ipynb'
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        expected_nb_path = os.path.join(dirname, 'test.ipynb')
        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(environ)
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        jupyter_notebook = _assert_find_executable('jupyter-notebook', path)
        assert cmd_exec.args == [jupyter_notebook, expected_nb_path, '--NotebookApp.default_url=/notebooks/test.ipynb']
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        "commands:\n default:\n    unix: echo 'pass'\nservices:\n    REDIS_URL: redis\npackages: ['notebook']\n",
        'test.ipynb':
        '{}',
        'envs/should_ignore_this.ipynb':
        '{}',
        'services/should_ignore_this.ipynb':
        '{}',
        '.should_ignore_dotfile.ipynb':
        '{}',
        '.should_ignore_dotdir/foo.ipynb':
        '{}'
    }, check_notebook_guess_command)

    # anaconda-project run data.ipynb


def test_notebook_guess_command_can_be_default():
    def check_notebook_guess_command_can_be_default(dirname):
        project = project_no_dedicated_env(dirname)

        assert [
            "%s: No commands run notebooks a.ipynb, b.ipynb, c.ipynb, d/d.ipynb, e.ipynb, f.ipynb" %
            project.project_file.basename
        ] == project.suggestions

        project.suggestion_objects[0].fix(project)
        project.project_file.save()

        assert [] == project.problems
        assert len(project.commands) == 6
        assert project.default_command is not None
        assert project.default_command.notebook == 'a.ipynb'
        assert ['a.ipynb', 'b.ipynb', 'c.ipynb', 'd/d.ipynb', 'e.ipynb',
                'f.ipynb'] == sorted([c for c in project.commands])

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "packages: ['notebook']\n",
            # we pick the first command alphabetically in this case
            # so the test looks for that
            'a.ipynb': '{}',
            'b.ipynb': '{}',
            'c.ipynb': '{}',
            'd/d.ipynb': '{}',
            'e.ipynb': '{}',
            'f.ipynb': '{}'
        },
        check_notebook_guess_command_can_be_default)


def test_notebook_command_jupyter_not_on_path(monkeypatch):
    def check_notebook_command(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.notebook == 'test.ipynb'

        environ = minimal_environ(PROJECT_DIR=dirname)

        def mock_find_executable(*args, **kwargs):
            return None

        monkeypatch.setattr('distutils.spawn.find_executable', mock_find_executable)

        cmd_exec = command.exec_info_for_environment(environ)
        assert 'jupyter-notebook' in cmd_exec.args[0]
        assert cmd_exec.args[1:] == [
            os.path.join(dirname, 'test.ipynb'), '--NotebookApp.default_url=/notebooks/test.ipynb'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    notebook: test.ipynb\n"
        }, check_notebook_command)


def test_multiple_notebooks_suggestion_rejected():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert ["%s: No commands run notebooks foo/test2.ipynb, test.ipynb" % project.project_file.basename
                ] == project.suggestions

        project.suggestion_objects[0].no_fix(project)
        project.project_file.save()

        assert 'test.ipynb' not in project.commands
        assert 'foo/test2.ipynb' not in project.commands
        assert 'default' in project.commands
        assert len(project.commands) == 1  # we should have ignored all the ignored ones
        assert project.default_command.name == 'default'

        # now we're skipping the import so it should have gone away
        assert project.suggestions == []

        assert ['foo/test2.ipynb', 'test.ipynb'] == project.project_file.get_value(['skip_imports', 'notebooks'])

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        "commands:\n default:\n    unix: echo 'pass'\nservices:\n    REDIS_URL: redis\npackages: ['notebook']\n",
        'test.ipynb':
        '{}',
        'foo/test2.ipynb':
        '{}',
        'envs/should_ignore_this.ipynb':
        'pretend this is more notebook data',
        'services/should_ignore_this.ipynb':
        'pretend this is more notebook data',
        '.should_ignore_dotfile.ipynb':
        'moar fake notebook',
        '.should_ignore_dotdir/foo.ipynb':
        'still moar fake notebook'
    }, check)


def test_skip_all_notebook_imports():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert [] == project.problems
        assert ["%s: No command runs notebook test.ipynb" % project.project_file.basename] == project.suggestions

        project.project_file.set_value(['skip_imports', 'notebooks'], True)
        project.project_file.save()

        assert [] == project.suggestions
        assert [] == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        "commands:\n default:\n    unix: echo 'pass'\nservices:\n    REDIS_URL: redis\npackages: ['notebook']\n",
        'test.ipynb':
        '{}'
    }, check)


def test_invalid_skip_imports_notebooks():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        assert "'skip_imports: notebooks:' value should be a list" in project.problems[0]
        assert [] == project.suggestions

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: ("commands:\n default:\n    unix: echo 'pass'\nservices:\n" +
                                   "  REDIS_URL: redis\npackages: ['notebook']\nskip_imports:\n  notebooks: {}\n")
    }, check)


def test_single_notebook_suggestion_rejected():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert ["%s: No command runs notebook test.ipynb" % project.project_file.basename] == project.suggestions

        project.suggestion_objects[0].no_fix(project)
        project.project_file.save()

        assert 'test.ipynb' not in project.commands
        assert 'default' in project.commands
        assert len(project.commands) == 1  # we should have ignored all the ignored ones
        assert project.default_command.name == 'default'

        # now we're skipping the import so it should have gone away
        assert project.suggestions == []

        assert ['test.ipynb'] == project.project_file.get_value(['skip_imports', 'notebooks'])

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        "commands:\n default:\n    unix: echo 'pass'\nservices:\n    REDIS_URL: redis\npackages: ['notebook']\n",
        'test.ipynb':
        '{}',
        'envs/should_ignore_this.ipynb':
        'pretend this is more notebook data',
        'services/should_ignore_this.ipynb':
        'pretend this is more notebook data',
        '.should_ignore_dotfile.ipynb':
        'moar fake notebook',
        '.should_ignore_dotdir/foo.ipynb':
        'still moar fake notebook'
    }, check)


def test_notebook_command_conflict():
    def check_notebook_conflict_command(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: command '%s' has multiple commands in it, 'notebook' can't go with 'unix'" % (
            project.project_file.basename, 'default')
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    notebook: test.ipynb\n    unix: echo 'pass'"
        }, check_notebook_conflict_command)


def test_bokeh_command_conflict():
    def check_bokeh_conflict_command(dirname):
        project = project_no_dedicated_env(dirname)
        assert 1 == len(project.problems)
        expected_error = "%s: command '%s' has multiple commands in it, 'bokeh_app' can't go with 'unix'" % (
            project.project_file.basename, 'default')
        assert expected_error == project.problems[0]

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    bokeh_app: app.py\n    unix: echo 'pass'"
        }, check_bokeh_conflict_command)


def test_bokeh_command():
    def check_bokeh_command(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.bokeh_app == 'test.py'
        assert command.notebook is None
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(environ)
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        bokeh = _assert_find_executable('bokeh', path)
        assert cmd_exec.args == [bokeh, 'serve', os.path.join(dirname, 'test.py'), '--show']
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    bokeh_app: test.py\n"
        }, check_bokeh_command)


def test_bokeh_command_with_extra_args():
    def check_bokeh_command_extra_args(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.bokeh_app == 'test.py'
        assert command.notebook is None
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(environ, extra_args=['--foo'])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        bokeh = _assert_find_executable('bokeh', path)
        assert cmd_exec.args == [bokeh, 'serve', os.path.join(dirname, 'test.py'), '--show', '--foo']
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    bokeh_app: test.py\n"
        }, check_bokeh_command_extra_args)


def test_bokeh_command_with_project_http_args():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.bokeh_app == 'test.py'
        assert command.notebook is None
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None
        assert command.supports_http_options

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ,
            extra_args=[
                '--foo', '--anaconda-project-url-prefix', 'blah', '--anaconda-project-port', '1234',
                '--anaconda-project-host', 'example.com', '--anaconda-project-no-browser',
                '--anaconda-project-iframe-hosts=foo1.com *.foo2.com', '--anaconda-project-use-xheaders',
                '--anaconda-project-address', '1.2.3.4'
            ])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        bokeh = _assert_find_executable('bokeh', path)
        assert cmd_exec.args == [
            bokeh, 'serve',
            os.path.join(dirname, 'test.py'), '--address', '1.2.3.4', '--host', 'example.com', '--port', '1234',
            '--prefix', 'blah', '--use-xheaders', '--foo'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    bokeh_app: test.py\n"
        }, check)


def test_bokeh_command_with_multiple_host_args():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.bokeh_app == 'test.py'
        assert command.notebook is None
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None
        assert command.supports_http_options

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ, extra_args=['--anaconda-project-host', 'example.com', '--anaconda-project-host', 'example2.com'])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        bokeh = _assert_find_executable('bokeh', path)
        assert cmd_exec.args == [
            bokeh, 'serve',
            os.path.join(dirname, 'test.py'), '--host', 'example.com', '--host', 'example2.com', '--show'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    bokeh_app: test.py\n"
        }, check)


def test_bokeh_command_with_multiple_iframe_hosts_args():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.bokeh_app is None
        assert command.notebook == "test.ipynb"
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None
        assert command.supports_http_options

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ,
            extra_args=[
                '--anaconda-project-iframe-hosts', 'example.com', '--anaconda-project-iframe-hosts',
                'foo1.com *.foo2.com'
            ])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        jupyter = _assert_find_executable('jupyter-notebook', path)
        assert cmd_exec.args == [
            jupyter,
            os.path.join(dirname, 'test.ipynb'), '--NotebookApp.default_url=/notebooks/test.ipynb',
            '--NotebookApp.tornado_settings=' +
            """{ 'headers': { 'Content-Security-Policy': "frame-ancestors 'self' example.com foo1.com *.foo2.com" } }"""
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    notebook: test.ipynb\n"
        }, check)


def test_bokeh_command_with_value_missing_for_project_http_args():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.bokeh_app == 'test.py'
        assert command.notebook is None
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None
        assert command.supports_http_options

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ,
            extra_args=[
                '--foo', '--anaconda-project-url-prefix', '--anaconda-project-port', '--anaconda-project-host',
                '--anaconda-project-address'
            ])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        bokeh = _assert_find_executable('bokeh', path)
        assert cmd_exec.args == [
            bokeh, 'serve',
            os.path.join(dirname, 'test.py'), '--address', '', '--host', '', '--show', '--port', '', '--prefix', '',
            '--foo'
        ]
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    bokeh_app: test.py\n"
        }, check)


def test_bokeh_command_with_disabled_project_http_args():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        command = project.default_command
        assert command.bokeh_app == 'test.py'
        assert command.notebook is None
        assert command.unix_shell_commandline is None
        assert command.windows_cmd_commandline is None
        assert command.conda_app_entry is None
        assert not command.supports_http_options

        environ = minimal_environ(PROJECT_DIR=dirname)
        cmd_exec = command.exec_info_for_environment(
            environ,
            extra_args=[
                '--foo', '--anaconda-project-url-prefix', 'blah', '--anaconda-project-port', '1234',
                '--anaconda-project-host', 'example.com', '--anaconda-project-no-browser',
                '--anaconda-project-use-xheaders'
            ])
        path = os.pathsep.join([environ['PROJECT_DIR'], environ['PATH']])
        bokeh = _assert_find_executable('bokeh', path)
        assert cmd_exec.args == [bokeh, 'serve', os.path.join(dirname, 'test.py'), '--foo']
        assert cmd_exec.shell is False

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n default:\n    bokeh_app: test.py\n    supports_http_options: false\n"
        }, check)


def test_run_argv_from_project_file_app_entry():
    def check_run_argv(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        command = project.default_command
        assert command.name == 'foo'
        assert command.conda_app_entry == "foo bar ${PREFIX}"

        assert 1 == len(project.commands)
        assert 'foo' in project.commands
        assert project.commands['foo'] is command

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
commands:
  foo:
    conda_app_entry: foo bar ${PREFIX}
"""
        }, check_run_argv)


def test_run_argv_from_project_file_shell():
    def check_run_argv(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        command = project.default_command
        assert command.name == 'foo'
        assert command.unix_shell_commandline == "foo bar ${PREFIX}"

        assert 1 == len(project.commands)
        assert 'foo' in project.commands
        assert project.commands['foo'] is command

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
commands:
  foo:
    unix: foo bar ${PREFIX}
"""
        }, check_run_argv)


def test_run_argv_from_project_file_windows(monkeypatch):
    def check_run_argv(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        command = project.default_command
        assert command.name == 'foo'
        assert command.windows_cmd_commandline == "foo bar %CONDA_DEFAULT_ENV%"
        assert command.unix_shell_commandline is None

        assert 1 == len(project.commands)
        assert 'foo' in project.commands
        assert project.commands['foo'] is command

        def mock_platform_system():
            return 'Windows'

        monkeypatch.setattr('platform.system', mock_platform_system)

        environ = minimal_environ(PROJECT_DIR=dirname)

        exec_info = project.default_exec_info_for_environment(environ)
        assert exec_info.shell

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
commands:
  foo:
    windows: foo bar %CONDA_DEFAULT_ENV%
"""
        }, check_run_argv)


def test_exec_info_is_none_when_no_commands():
    def check_exec_info(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        command = project.default_command
        assert command is None

        environ = minimal_environ(PROJECT_DIR=dirname)

        exec_info = project.default_exec_info_for_environment(environ)
        assert exec_info is None

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: """
"""}, check_exec_info)


def test_exec_info_is_none_when_command_not_for_our_platform():
    def check_exec_info(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        command = project.default_command
        assert command is not None
        assert command.name == 'foo'

        environ = minimal_environ(PROJECT_DIR=dirname)

        exec_info = project.default_exec_info_for_environment(environ)
        assert exec_info is None

    import platform
    not_us = 'windows'
    if platform.system() == 'Windows':
        not_us = 'unix'
    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
commands:
  foo:
    %s: foo
""" % not_us
        }, check_exec_info)


def _run_argv_for_environment(environ=None, expected_output=None, chdir=False, os_specific=False, extra_args=None):
    environ = minimal_environ(**(environ or {}))

    prefix_var = conda_api.conda_prefix_variable() if os_specific else 'PREFIX'
    if platform.system() == 'Windows':
        echo_stuff = 'echo_stuff.bat'
        echo_path = '%PROJECT_DIR%\\\\' + echo_stuff
        prefix_var = '%{}%'.format(prefix_var)
        command_name = 'windows'
    else:
        echo_stuff = 'echo_stuff.sh'
        echo_path = '${PROJECT_DIR}/' + echo_stuff
        prefix_var = '${%s}' % prefix_var
        command_name = 'unix'
    if not os_specific:
        command_name = 'conda_app_entry'
        echo_path = echo_stuff
    if expected_output is None:
        args = ' '.join(['foo', 'bar'] + (extra_args or []))
        expected_output = conda_api.environ_get_prefix(os.environ) + ' ' + args
    command_line = '%s: "%s %s foo bar"' % (command_name, echo_path, prefix_var)
    print('YAML command line: %s' % command_line)
    print('Expected output: %s' % expected_output)
    print('Current directory: %s' % os.getcwd())
    print('Path: %s' % os.environ.get('PATH'))

    def check_echo_output(dirname):
        if 'PROJECT_DIR' not in environ:
            environ['PROJECT_DIR'] = dirname
        os.chmod(os.path.join(dirname, echo_stuff), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
        old_dir = None
        if chdir:
            old_dir = os.getcwd()
            os.chdir(dirname)
        try:
            project = project_no_dedicated_env(dirname)
            assert [] == project.problems
            exec_info = project.default_exec_info_for_environment(environ, extra_args=extra_args)
            if exec_info.shell:
                args = exec_info.args[0]
            else:
                args = exec_info.args
            print('Command args: %s' % repr(args))
            print('Using shell: %s' % str(exec_info.shell))
            output = subprocess.check_output(args, shell=exec_info.shell, env=environ).decode()
            # strip() removes \r\n or \n so we don't have to deal with the difference
            print('Actual output: %s' % output.strip())
            assert output.strip() == expected_output.format(dirname=dirname)
        finally:
            if old_dir is not None:
                print("Changing back to %s from %s" % (old_dir, dirname), file=sys.stderr)
                os.chdir(old_dir)
            # this should happen automatically but it's failing sometimes on
            # Windows maybe because of a race where echo_stuff.bat is still
            # in use.
            batscript = os.path.join(dirname, "echo_stuff.bat")
            attempts = 6
            print("os.path.exists(%s) = %s" % (batscript, repr(os.path.exists(batscript))), file=sys.stderr)
            print("before remove, os.stat(%s) = %r" % (batscript, os.stat(batscript)))
            print("before remove, os.stat(%s) = %r" % (dirname, os.stat(dirname)))
            while attempts > 0 and os.path.exists(batscript):
                try:
                    print("Attempting to remove %s" % (batscript), file=sys.stderr)
                    os.remove(batscript)
                except Exception as e:
                    print("Failed to remove %s: %s" % (batscript, str(e)), file=sys.stderr)
                    time.sleep(1)
                    attempts = attempts - 1
                else:
                    print("os.remove did not throw removing %s" % (batscript), file=sys.stderr)
                    print(
                        "after remove, os.path.exists(%s) = %s" % (batscript, repr(os.path.exists(batscript))),
                        file=sys.stderr)
                    try:
                        print("after remove, os.stat(%s) = %r" % (batscript, os.stat(batscript)))
                    except Exception:
                        print("after remove, could not stat the script")

                    try:
                        print("after remove, files in %s: %r" % (dirname, os.listdir(dirname)), file=sys.stderr)
                    except Exception:
                        pass

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
commands:
  default:
    %s
""" % command_line,
            "echo_stuff.sh": """#!/bin/sh
echo "$*"
""",
            "echo_stuff.bat": """
@echo off
echo %*
"""
        }, check_echo_output)


def test_run_command_in_project_dir():
    # We can't use a generic command name with Windows batch files
    if platform.system() != 'Windows':
        _run_argv_for_environment(os_specific=False, extra_args=[])


def test_run_command_in_project_dir_extra_args():
    # We can't use a generic command name with Windows batch files
    if platform.system() != 'Windows':
        _run_argv_for_environment(os_specific=False, extra_args=["baz"])


def test_run_command_in_project_dir_os_specific(monkeypatch):
    _run_argv_for_environment(os_specific=True, extra_args=[])


def test_run_command_in_project_dir_os_specific_extra_args(monkeypatch):
    _run_argv_for_environment(os_specific=True, extra_args=["baz"])


def test_run_command_in_project_dir_os_specific_and_cwd_is_project_dir():
    # We can't use a generic command name with Windows batch files
    _run_argv_for_environment(os_specific=True, extra_args=[], chdir=True)


def test_run_command_in_project_dir_os_specific_with_conda_env():
    _run_argv_for_environment(
        dict(CONDA_PREFIX='/someplace', CONDA_ENV_PATH='/someplace', CONDA_DEFAULT_ENV='/someplace'),
        "/someplace foo bar",
        os_specific=True)


def test_run_command_is_on_system_path():
    def check_python_version_output(dirname):
        environ = minimal_environ(PROJECT_DIR=dirname)
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        exec_info = project.default_exec_info_for_environment(environ)
        output = subprocess.check_output(exec_info.args, shell=exec_info.shell, stderr=subprocess.STDOUT).decode()
        assert output.startswith("Python")

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
commands:
  default:
    conda_app_entry: python --version
"""
        }, check_python_version_output)


def test_run_command_does_not_exist():
    def check_error_on_nonexistent_path(dirname):
        import errno
        environ = minimal_environ(PROJECT_DIR=dirname)
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        exec_info = project.default_exec_info_for_environment(environ)
        assert exec_info.args[0] == 'this-command-does-not-exist'
        try:
            FileNotFoundError  # noqa
        except NameError:
            # python 2
            FileNotFoundError = OSError
        with pytest.raises(FileNotFoundError) as excinfo:
            subprocess.check_output(exec_info.args, stderr=subprocess.STDOUT, shell=exec_info.shell).decode()
        assert excinfo.value.errno == errno.ENOENT

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
commands:
  default:
    conda_app_entry: this-command-does-not-exist
"""
        }, check_error_on_nonexistent_path)


def test_run_command_stuff_missing_from_environment():
    def check_run_with_stuff_missing(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        environ = minimal_environ(PROJECT_DIR=dirname)
        conda_var = conda_api.conda_prefix_variable()
        for key in ('PATH', conda_var, 'PROJECT_DIR'):
            environ_copy = deepcopy(environ)
            del environ_copy[key]
            with pytest.raises(ValueError) as excinfo:
                project.default_exec_info_for_environment(environ_copy)
            assert ('%s must be set' % key) in repr(excinfo.value)

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: """
commands:
  default:
    conda_app_entry: foo
"""
        }, check_run_with_stuff_missing)


def test_get_publication_info_from_empty_project():
    def check_publication_info_from_empty(dirname):
        project = project_no_dedicated_env(dirname)
        expected = {
            'name': os.path.basename(dirname),
            'url_friendly_name': os.path.basename(dirname),
            'description': '',
            'anaconda_project_version': version,
            'commands': {},
            'env_specs': {
                'default': {
                    'channels': [],
                    'packages': [],
                    'description': 'Default',
                    'locked': False,
                    'platforms': [],
                    'variables': {},
                    'downloads': {},
                    'services': {}
                }
            },
        }
        assert expected == project.publication_info()

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME: """
env_specs:
  default:
    description: "Default"
    """
    }, check_publication_info_from_empty)


_complicated_project_contents = """
name: foo bar
description: "A very complicated project."

commands:
  foo:
    unix: echo hi
    description: "say hi"
    supports_http_options: true
  bar:
    windows: echo boo
    env_spec: lol
  baz:
    conda_app_entry: echo blah
  myapp:
    bokeh_app: main.py
    env_spec: woot
  foo.ipynb:
    description: 'Notebook foo.ipynb'
    notebook: foo.ipynb
    registers_fusion_function: true

packages:
  - foo

channels:
  - bar

env_specs:
  default:
    description: "Default"
    packages:
      - notebook
  woot:
    packages:
      - blah
      - bokeh
    channels:
      - woohoo
  w00t:
    description: "double 0"
    packages:
      - something
  lol: {}

downloads:
  FOO: https://example.com/blah

services:
  REDIS_URL: redis

variables:
  SOMETHING: {}
  SOMETHING_ELSE:
     default: 42
"""


def test_get_publication_info_from_complex_project():
    def check_publication_info_from_complex(dirname):
        project = project_no_dedicated_env(dirname)

        expected = {
            'name': 'foo bar',
            'url_friendly_name': 'foo-bar',
            'description': 'A very complicated project.',
            'anaconda_project_version': version,
            'commands': {
                'bar': {
                    'description': 'echo boo',
                    'env_spec': 'lol',
                    'windows': 'echo boo',
                    'supports_http_options': False
                },
                'baz': {
                    'description': 'echo blah',
                    'env_spec': 'default',
                    'supports_http_options': False
                },
                'foo': {
                    'description': 'say hi',
                    'default': True,
                    'env_spec': 'default',
                    'unix': 'echo hi',
                    'supports_http_options': True
                },
                'myapp': {
                    'description': 'Bokeh app main.py',
                    'bokeh_app': 'main.py',
                    'env_spec': 'woot',
                    'supports_http_options': True
                },
                'foo.ipynb': {
                    'description': 'Notebook foo.ipynb',
                    'notebook': 'foo.ipynb',
                    'env_spec': 'default',
                    'supports_http_options': True,
                    'registers_fusion_function': True
                }
            },
            'env_specs': {
                'default': {
                    'channels': ['bar'],
                    'packages': ['foo', 'notebook'],
                    'description': 'Default',
                    'locked': False,
                    'platforms': ['linux-64', 'osx-64', 'win-64'],
                    'downloads': {
                        'FOO': {
                            'encrypted': False,
                            'title': 'FOO',
                            'description': 'A downloaded file which is referenced by FOO.',
                            'url': 'https://example.com/blah'
                        }
                    },
                    'variables': {
                        'SOMETHING': {
                            'encrypted': False,
                            'title': 'SOMETHING',
                            'description': 'SOMETHING environment variable must be set.'
                        },
                        'SOMETHING_ELSE': {
                            'encrypted': False,
                            'title': 'SOMETHING_ELSE',
                            'description': 'SOMETHING_ELSE environment variable must be set.',
                            'default': "42"
                        }
                    },
                    'services': {
                        'REDIS_URL': {
                            'title': 'REDIS_URL',
                            'description': 'A running Redis server, located by a redis: URL set as REDIS_URL.',
                            'type': 'redis',
                            'encrypted': False
                        }
                    }
                },
                'lol': {
                    'channels': ['bar'],
                    'packages': ['foo'],
                    'description': 'lol',
                    'locked': False,
                    'platforms': ['linux-64', 'osx-64', 'win-64'],
                    'downloads': {
                        'FOO': {
                            'encrypted': False,
                            'title': 'FOO',
                            'description': 'A downloaded file which is referenced by FOO.',
                            'url': 'https://example.com/blah'
                        }
                    },
                    'variables': {
                        'SOMETHING': {
                            'encrypted': False,
                            'title': 'SOMETHING',
                            'description': 'SOMETHING environment variable must be set.'
                        },
                        'SOMETHING_ELSE': {
                            'encrypted': False,
                            'title': 'SOMETHING_ELSE',
                            'description': 'SOMETHING_ELSE environment variable must be set.',
                            'default': "42"
                        }
                    },
                    'services': {
                        'REDIS_URL': {
                            'title': 'REDIS_URL',
                            'description': 'A running Redis server, located by a redis: URL set as REDIS_URL.',
                            'type': 'redis',
                            'encrypted': False
                        }
                    }
                },
                'w00t': {
                    'channels': ['bar'],
                    'packages': ['foo', 'something'],
                    'description': 'double 0',
                    'locked': False,
                    'platforms': ['linux-64', 'osx-64', 'win-64'],
                    'downloads': {
                        'FOO': {
                            'encrypted': False,
                            'title': 'FOO',
                            'description': 'A downloaded file which is referenced by FOO.',
                            'url': 'https://example.com/blah'
                        }
                    },
                    'variables': {
                        'SOMETHING': {
                            'encrypted': False,
                            'title': 'SOMETHING',
                            'description': 'SOMETHING environment variable must be set.'
                        },
                        'SOMETHING_ELSE': {
                            'encrypted': False,
                            'title': 'SOMETHING_ELSE',
                            'description': 'SOMETHING_ELSE environment variable must be set.',
                            'default': "42"
                        }
                    },
                    'services': {
                        'REDIS_URL': {
                            'title': 'REDIS_URL',
                            'description': 'A running Redis server, located by a redis: URL set as REDIS_URL.',
                            'type': 'redis',
                            'encrypted': False
                        }
                    }
                },
                'woot': {
                    'channels': ['bar', 'woohoo'],
                    'packages': ['foo', 'blah', 'bokeh'],
                    'description': 'woot',
                    'locked': False,
                    'platforms': ['linux-64', 'osx-64', 'win-64'],
                    'downloads': {
                        'FOO': {
                            'encrypted': False,
                            'title': 'FOO',
                            'description': 'A downloaded file which is referenced by FOO.',
                            'url': 'https://example.com/blah'
                        }
                    },
                    'variables': {
                        'SOMETHING': {
                            'encrypted': False,
                            'title': 'SOMETHING',
                            'description': 'SOMETHING environment variable must be set.'
                        },
                        'SOMETHING_ELSE': {
                            'encrypted': False,
                            'title': 'SOMETHING_ELSE',
                            'description': 'SOMETHING_ELSE environment variable must be set.',
                            'default': "42"
                        }
                    },
                    'services': {
                        'REDIS_URL': {
                            'title': 'REDIS_URL',
                            'description': 'A running Redis server, located by a redis: URL set as REDIS_URL.',
                            'type': 'redis',
                            'encrypted': False
                        }
                    }
                }
            }
        }

        assert expected == project.publication_info()
        assert len(project.suggestions) == 0

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: _complicated_project_contents,
        "main.py": "",
        "foo.ipynb": ""
    }, check_publication_info_from_complex)


def test_find_requirements():
    def check_find_requirements(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems

        spec_name = project.default_env_spec_name

        reqs = project.find_requirements(spec_name, env_var='SOMETHING')
        assert len(reqs) == 1
        assert reqs[0].env_var == 'SOMETHING'

        reqs = project.find_requirements(spec_name, klass=CondaEnvRequirement)
        assert len(reqs) == 1
        assert isinstance(reqs[0], CondaEnvRequirement)

        reqs = project.find_requirements(spec_name, klass=ServiceRequirement)
        assert len(reqs) == 1
        assert isinstance(reqs[0], ServiceRequirement)

        reqs = project.find_requirements(spec_name, klass=DownloadRequirement)
        assert len(reqs) == 1
        assert isinstance(reqs[0], DownloadRequirement)

        # the klass and env_var kwargs must be "AND"-ed together
        reqs = project.find_requirements(spec_name, klass=CondaEnvRequirement, env_var='SOMETHING')
        assert [] == reqs

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: _complicated_project_contents,
        "main.py": "",
        "foo.ipynb": ""
    }, check_find_requirements)


def test_requirements_subsets():
    def check_requirements_subsets(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems

        services = project.service_requirements(project.default_env_spec_name)
        assert len(services) == 1
        assert isinstance(services[0], ServiceRequirement)
        assert services[0].env_var == 'REDIS_URL'

        downloads = project.download_requirements(project.default_env_spec_name)
        assert len(downloads) == 1
        assert isinstance(downloads[0], DownloadRequirement)
        assert downloads[0].env_var == 'FOO'

        everything = project.all_variable_requirements(project.default_env_spec_name)
        everything_names = [req.env_var for req in everything]
        # the first element is CONDA_PREFIX, CONDA_ENV_PATH, or CONDA_DEFAULT_ENV
        assert ['FOO', 'REDIS_URL', 'SOMETHING', 'SOMETHING_ELSE'] == sorted(everything_names)[1:]

        plain = project.plain_variable_requirements(project.default_env_spec_name)
        plain_names = [req.env_var for req in plain]
        assert ['SOMETHING', 'SOMETHING_ELSE'] == sorted(plain_names)

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: _complicated_project_contents,
        "main.py": "",
        "foo.ipynb": ""
    }, check_requirements_subsets)


def test_env_var_name_list_properties():
    def check_env_var_name_list_properties(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems

        services = project.services(project.default_env_spec_name)
        assert ['REDIS_URL'] == services

        downloads = project.downloads(project.default_env_spec_name)
        assert ['FOO'] == downloads

        everything = project.all_variables(project.default_env_spec_name)
        # the first element is CONDA_PREFIX, CONDA_ENV_PATH, or CONDA_DEFAULT_ENV
        assert ['FOO', 'REDIS_URL', 'SOMETHING', 'SOMETHING_ELSE'] == sorted(everything)[1:]

        plain = project.plain_variables(project.default_env_spec_name)
        assert ['SOMETHING', 'SOMETHING_ELSE'] == sorted(plain)

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: _complicated_project_contents,
        "main.py": "",
        "foo.ipynb": ""
    }, check_env_var_name_list_properties)


def test_project_problem():
    unfixable = ProjectProblem(text="foo")
    assert unfixable.text == "foo"
    assert not unfixable.can_fix
    assert unfixable.fix(None) is None

    def fixit(project):
        return 42

    fixable = ProjectProblem(text="bar", fix_prompt="prompt", fix_function=fixit)
    assert fixable.text == "bar"
    assert fixable.can_fix
    assert fixable.fix(None) == 42


def test_auto_fix_missing_name():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert len(project.problems) == 1
        assert len(project.problem_objects) == 1
        problem = project.problem_objects[0]
        assert problem.text == ("%s: The 'name:' field is missing." % DEFAULT_PROJECT_FILENAME)
        assert problem.can_fix

        problem.fix(project)
        project.project_file.save()

        assert project.problems == []
        assert project.name == os.path.basename(dirname)
        assert 'name' in project.project_file.root

    with_directory_contents(
        {
            DEFAULT_PROJECT_FILENAME: "env_specs:\n  default: {}\nplatforms: [linux-32,linux-64,osx-64,win-32,win-64]\n"
        }, check)


def test_auto_fix_env_spec_import():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert len(project.problems) == 1
        assert len(project.problem_objects) == 1
        assert len(project.fixable_problems) == 1
        problem = project.problem_objects[0]
        assert problem.text == "Environment spec 'stuff' from environment.yml is not in anaconda-project.yml."
        assert problem.can_fix

        problem.fix(project)
        project.project_file.save()

        assert project.problems == []
        assert list(project.env_specs.keys()) == ['stuff']
        spec = project.env_specs['stuff']
        assert spec.conda_packages == ('a', 'b')
        assert spec.pip_packages == ('foo', )
        assert spec.channels == ('bar', )

    with_directory_contents(
        {
            DEFAULT_PROJECT_FILENAME: "name: foo\nenv_specs: {}\nplatforms: [linux-32,linux-64,osx-64,win-32,win-64]\n",
            "environment.yml": """
name: stuff
dependencies:
 - a
 - b
 - pip:
   - foo
channels:
 - bar
"""
        }, check)


def test_auto_fix_requirements_txt_import():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert len(project.problems) == 1
        assert len(project.problem_objects) == 1
        assert len(project.fixable_problems) == 1
        problem = project.problem_objects[0]
        assert problem.text == "Environment spec 'default' from requirements.txt is not in anaconda-project.yml."
        assert problem.can_fix

        problem.fix(project)
        project.project_file.save()

        assert project.problems == []
        assert list(project.env_specs.keys()) == ['default']
        spec = project.env_specs['default']
        assert spec.conda_packages == ()
        assert spec.pip_packages == ('abc', 'efg')
        assert spec.channels == ()

    with_directory_contents(
        {
            DEFAULT_PROJECT_FILENAME: "name: foo\nenv_specs: {}\nplatforms: [linux-32,linux-64,osx-64,win-32,win-64]\n",
            "requirements.txt": """
# these are some pip packages.
abc
efg
"""
        }, check)


def test_auto_fix_env_spec_out_of_sync():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert len(project.problems) == 1
        assert len(project.problem_objects) == 1
        assert len(project.fixable_problems) == 1
        problem = project.problem_objects[0]
        assert ("Environment spec 'stuff' from environment.yml is out of sync with anaconda-project.yml. Diff:\n" +
                "  channels:\n    + bar\n+ a\n+ b\n  pip:\n    + foo") == problem.text
        assert problem.can_fix

        problem.fix(project)
        project.project_file.save()

        assert project.problems == []
        assert list(project.env_specs.keys()) == ['stuff']
        spec = project.env_specs['stuff']
        assert spec.conda_packages == ('a', 'b')
        assert spec.pip_packages == ('foo', )
        assert spec.channels == ('bar', )

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME:
        "name: foo\nenv_specs: { 'stuff': { 'packages':[] } }\nplatforms: [linux-32,linux-64,osx-64,win-32,win-64]\n",
        "environment.yml":
        """
name: stuff
dependencies:
 - a
 - b
 - pip:
   - foo
channels:
 - bar
"""
    }, check)


def test_auto_fix_env_spec_import_saying_no():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert len(project.problems) == 1
        assert len(project.problem_objects) == 1
        assert len(project.fixable_problems) == 1
        problem = project.problem_objects[0]
        assert problem.text == "Environment spec 'stuff' from environment.yml is not in anaconda-project.yml."
        assert problem.can_fix

        problem.no_fix(project)
        project.project_file.save()

        assert project.problems == []
        assert list(project.env_specs.keys()) == ['default']

        skip_importing_hash = project.project_file.get_value(['skip_imports', 'environment'])
        assert skip_importing_hash is not None
        assert skip_importing_hash != ''

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME:
        "name: foo\nenv_specs: {'default':{'packages':[]}}\nplatforms: [linux-32,linux-64,osx-64,win-32,win-64]\n",
        "environment.yml":
        """
name: stuff
dependencies:
 - a
 - b
 - pip:
   - foo
channels:
 - bar
"""
    }, check)


def test_no_auto_fix_env_spec_with_notebook_bokeh_injection():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert len(project.problems) == 1
        assert len(project.problem_objects) == 1
        assert len(project.fixable_problems) == 1
        problem = project.problem_objects[0]
        assert ("Environment spec 'stuff' from environment.yml is out of sync with anaconda-project.yml. Diff:\n" +
                "  channels:\n    + bar\n+ a\n+ b\n  pip:\n    + foo") == problem.text
        assert problem.can_fix

        problem.fix(project)
        project.project_file.save()

        assert project.problems == []
        assert list(project.env_specs.keys()) == ['stuff']
        spec = project.env_specs['stuff']
        assert spec.conda_packages == ('a', 'b')
        assert spec.pip_packages == ('foo', )
        assert spec.channels == ('bar', )

        # add bokeh and notebook, which we should ignore
        packages = project.project_file.get_value(['env_specs', 'stuff', 'packages'])
        packages.extend(['bokeh', 'notebook'])
        project.project_file.set_value(['env_specs', 'stuff', 'packages'], packages)
        project.project_file.save()

        # no problems despite the diff
        assert project.problems == []
        spec = project.env_specs['stuff']
        assert spec.conda_packages == ('a', 'b', 'bokeh', 'notebook')

        # add some other package, should NOT ignore
        packages = project.project_file.get_value(['env_specs', 'stuff', 'packages'])
        packages.extend(['someother'])
        project.project_file.set_value(['env_specs', 'stuff', 'packages'], packages)
        project.project_file.save()

        assert len(project.problems) == 1

        # remove that again
        packages = project.project_file.get_value(['env_specs', 'stuff', 'packages'])
        packages.remove('someother')
        project.project_file.set_value(['env_specs', 'stuff', 'packages'], packages)
        project.project_file.save()

        assert len(project.problems) == 0

        # add a channel, should NOT ignore
        channels = project.project_file.get_value(['env_specs', 'stuff', 'channels'])
        channels.append("boo")
        project.project_file.set_value(['env_specs', 'stuff', 'channels'], channels)
        project.project_file.save()

        assert len(project.problems) == 1
        spec = project.env_specs['stuff']
        assert spec.channels == ('bar', 'boo')

        # remove the channel
        channels = project.project_file.get_value(['env_specs', 'stuff', 'channels'])
        channels.remove("boo")
        project.project_file.set_value(['env_specs', 'stuff', 'channels'], channels)
        project.project_file.save()

        assert len(project.problems) == 0

        # add a pip package, should NOT ignore
        packages = project.project_file.get_value(['env_specs', 'stuff', 'packages'])
        pip_list = None
        for p in packages:
            if isinstance(p, dict):
                pip_list = p['pip']
        pip_list.append('someother')
        project.project_file.use_changes_without_saving()
        project.project_file.save()

        assert len(project.problems) == 1
        spec = project.env_specs['stuff']
        assert spec.pip_packages == ('foo', 'someother')

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME:
        "name: foo\nenv_specs: { 'stuff': { 'packages':[] } }\nplatforms: [linux-32,linux-64,osx-64,win-32,win-64]\n",
        "environment.yml":
        """
name: stuff
dependencies:
 - a
 - b
 - pip:
   - foo
channels:
 - bar
"""
    }, check)


def test_auto_fix_notebook_dep():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert len(project.problems) == 0
        assert len(project.problem_objects) == 0
        assert len(project.fixable_problems) == 0
        assert len(project.suggestions) == 1
        assert len(project.suggestion_objects) == 1
        assert len(project.env_specs['default'].conda_package_names_set) == 0

        problem = project.suggestion_objects[0]
        assert ("%s: Command foo.ipynb uses env spec default which does not have the packages: notebook" %
                project.project_file.basename) == problem.text
        assert problem.can_fix

        problem.fix(project)
        project.project_file.save()

        assert project.problems == []
        assert project.suggestions == []
        assert project.env_specs['default'].conda_package_names_set == set(['notebook'])

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: ('commands:\n'
                                       '  foo.ipynb:\n'
                                       '    notebook: foo.ipynb\n'),
            'foo.ipynb': 'not a real notebook'
        }, check)


def test_no_auto_fix_notebook_dep_if_we_have_anaconda():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert project.problems == []
        assert project.suggestions == []
        assert project.env_specs['default'].conda_package_names_set == set(['anaconda'])

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: ('packages:\n'
                                   ' - anaconda\n'
                                   'commands:\n'
                                   '  foo.ipynb:\n'
                                   '    notebook: foo.ipynb\n'),
        'foo.ipynb':
        'not a real notebook'
    }, check)


def test_no_auto_fix_notebook_dep_if_we_have_notebook():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert project.problems == []
        assert project.env_specs['default'].conda_package_names_set == set(['notebook'])

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME: ('packages:\n'
                                   ' - notebook\n'
                                   'commands:\n'
                                   '  foo.ipynb:\n'
                                   '    notebook: foo.ipynb\n'),
        'foo.ipynb':
        'not a real notebook'
    }, check)


def test_auto_fix_bokeh_dep():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert len(project.problems) == 0
        assert len(project.problem_objects) == 0
        assert len(project.fixable_problems) == 0
        assert len(project.suggestions) == 1
        assert len(project.suggestion_objects) == 1
        assert len(project.env_specs['default'].conda_package_names_set) == 0

        problem = project.suggestion_objects[0]
        assert ("%s: Command bokeh_test uses env spec default which does not have the packages: bokeh" %
                project.project_file.basename) == problem.text
        assert problem.can_fix

        problem.fix(project)
        project.project_file.save()

        assert project.problems == []
        assert project.env_specs['default'].conda_package_names_set == set(['bokeh'])

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: ('commands:\n'
                                       '  bokeh_test:\n'
                                       '    bokeh_app: main.py\n'),
            'main.py': 'hello'
        }, check)


def test_unknown_field_in_root():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        expected_suggestion = ("%s: Unknown field name 'somejunk'" % project.project_file.basename)
        assert [expected_suggestion] == project.suggestions

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: "somejunk: False\n"}, check)


def test_unknown_field_in_command():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        expected_suggestion = ("%s: Unknown field name 'somejunk'" % project.project_file.basename)
        assert [expected_suggestion] == project.suggestions

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "commands:\n  foo:\n    unix: something\n    somejunk: True\n"
        }, check)


def test_unknown_field_in_env_spec():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        expected_suggestion = ("%s: Unknown field name 'somejunk'" % project.project_file.basename)
        assert [expected_suggestion] == project.suggestions

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: "env_specs:\n  foo:\n    packages: [something]\n    somejunk: True\n"
        }, check)


def test_unknown_field_in_env_spec_allowed():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        assert [] == project.suggestions

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        "env_specs:\n  foo:\n    packages: [something]\n    somejunk: True\n    user_fields: [somejunk]\n"
    }, check)


def test_unknown_field_in_root_of_lock_file():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        expected_suggestion = ("%s: Unknown field name 'somejunk'" % project.lock_file.basename)
        assert [expected_suggestion] == project.suggestions

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_LOCK_FILENAME: "locking_enabled: false\nsomejunk: False\n"
        }, check)


def test_unknown_field_in_lock_set_of_lock_file():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems
        expected_suggestion = ("%s: Unknown field name 'somejunk'" % project.lock_file.basename)
        assert [expected_suggestion] == project.suggestions

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_LOCK_FILENAME:
        """
env_specs:
  default:
     platforms: [linux-64,osx-64,win-64]
     somejunk: True
    """
    }, check)


def test_empty_file_has_problems():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert ["anaconda-project.yml: The 'name:' field is missing."] == project.problems

    with_directory_contents({DEFAULT_PROJECT_FILENAME: ""}, check)


def test_with_one_locked_env_spec_has_problems():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [
            "anaconda-project.yml: The 'name:' field is missing.",
            "anaconda-project.yml: The 'platforms:' field should list platforms the project supports."
        ] == project.problems

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME: """
env_specs:
  foo:
     packages: []
""",
        DEFAULT_PROJECT_LOCK_FILENAME: "locking_enabled: true\n"
    }, check)


def test_with_locking_enabled_no_env_specs_has_problems():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [
            "anaconda-project.yml: The 'name:' field is missing.",
            "anaconda-project.yml: The 'platforms:' field should list platforms the project supports."
        ] == project.problems

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME: "",
        DEFAULT_PROJECT_LOCK_FILENAME: "locking_enabled: true\n"
    }, check)


def test_with_locking_disabled_no_platforms_required():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME: "name: foo\nenv_specs:\n  default:\n    packages: []\n",
        DEFAULT_PROJECT_LOCK_FILENAME: "locking_enabled: false\n"
    }, check)


def test_load_weird_platform():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert [
            "anaconda-project.yml: Unusual platform name 'weird-valid' may be a typo "
            "(more usual examples: linux-64, osx-64, win-64)"
        ] == project.suggestions
        assert [
            "anaconda-project.yml: Platform name 'weirdinvalid' is invalid "
            "(valid examples: linux-64, osx-64, win-64)"
        ] == project.problems

        spec = project.env_specs['default']
        assert spec.platforms == ('linux-64', 'weird-valid')

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME:
        """
name: foo
platforms: [linux-64, weird-valid, weirdinvalid]
env_specs:
  default:
    packages: [foo]
"""
    }, check)


def test_only_some_env_specs_have_platforms_locking_disabled():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME:
        """
name: foo

env_specs:
  default:
    platforms: [linux-64]
    packages: [foo]
  no_platforms:
    packages: [bar]
"""
    }, check)


def test_only_some_env_specs_have_platforms_locking_enabled():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [("anaconda-project.yml: Env spec no_platforms does not have anything in its " + "'platforms:' field.")
                ] == project.problems

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME:
        """
name: foo

env_specs:
  default:
    platforms: [linux-64]
    packages: [foo]
  no_platforms:
    packages: [bar]
        """,
        DEFAULT_PROJECT_LOCK_FILENAME:
        "locking_enabled: true\n"
    }, check)


def test_empty_lock_file_enables_locking():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert project.locking_globally_enabled

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_LOCK_FILENAME: """
"""}, check)


def test_default_lock_file_disables_locking():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert not project.locking_globally_enabled

    with_directory_contents_completing_project_file(dict(), check)


def test_lock_file_non_bool_locking_enabled():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_error = "%s: Value for locking_enabled should be true or false, found %r" % (
            project.lock_file.basename, 42)
        assert [expected_error] == project.problems

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_LOCK_FILENAME: "locking_enabled:  42\n"}, check)


def test_lock_file_non_dict_env_specs():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_error = ("%s: 'env_specs:' section in lock file should be a dictionary " +
                          "from env spec names to lock information, found %r") % (project.lock_file.basename, 42)
        assert [expected_error] == project.problems

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_LOCK_FILENAME: "env_specs:  42\n"}, check)


def test_lock_file_non_dict_lock_set():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_error = ("%s: Field 'default' in env_specs in lock file should be a dictionary, " + "found %r") % (
            project.lock_file.basename, 42)
        assert [expected_error] == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_LOCK_FILENAME: """
env_specs:
  default: 42
"""
    }, check)


def test_lock_file_non_bool_lock_set_locked():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_error = ("%s: Value for locked for env spec 'default' should be true or false, " + "found %r") % (
            project.lock_file.basename, 42)
        assert [expected_error] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_LOCK_FILENAME: """
env_specs:
  default:
    locked: 42
"""
        }, check)


def test_lock_file_non_string_lock_set_hash():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_error = ("%s: Value for env_spec_hash for env spec 'default' should be a string, " + "found %r") % (
            project.lock_file.basename, 42)
        assert [expected_error] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_LOCK_FILENAME: """
env_specs:
  default:
    env_spec_hash: 42
"""
        }, check)


def test_lock_file_non_dict_lock_set_packages():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        expected_error = ("%s: 'packages:' section in env spec 'default' in lock file should be a dictionary, " +
                          "found %r") % (project.lock_file.basename, 42)
        assert [expected_error] == project.problems

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_LOCK_FILENAME: """
env_specs:
  default:
    packages: 42
"""
        }, check)


def test_lock_file_has_pip_packages():
    def check(dirname):
        project = project_no_dedicated_env(dirname)
        assert [] == project.problems

        expected_warning = ("%s: env spec 'default': pip dependencies are currently ignored in the lock file") % (
            project.lock_file.basename, )
        assert [expected_warning] == project.suggestions

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_LOCK_FILENAME:
        """
env_specs:
  default:
    platforms: [linux-64,osx-64,win-64]
    packages:
      all:
        - pip:
          - foobar
"""
    }, check)


def test_lock_file_has_invalid_packages():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        filename = project.lock_file.basename
        assert [
            "%s: invalid package specification: =" % filename,
            "%s: invalid package specification: foo bar" % filename,
            "%s: invalid pip package specifier: %%" % filename
        ] == project.problems

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_LOCK_FILENAME:
        """
env_specs:
  default:
    packages:
      all:
        - "="
        - foo bar
        - pip:
          - "%"
"""
    }, check)


def test_lock_file_has_wrong_platforms():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert [] == project.problems
        assert [
            "anaconda-project-lock.yml: Env spec 'default' specifies platforms "
            "'linux-64,osx-64,win-64' but the lock file has locked "
            "versions for platforms 'win-64'"
        ] == project.suggestions

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_LOCK_FILENAME:
        """
env_specs:
  default:
    platforms: ["win-64"]
    packages:
      all: [foo]
"""
    }, check)


def test_lock_file_has_zero_platforms():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert [] == project.problems
        assert [
            "anaconda-project-lock.yml: Env spec 'default' specifies platforms "
            "'linux-64,osx-64,win-64' but the lock file lists no platforms for it"
        ] == project.suggestions

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_LOCK_FILENAME: """
env_specs:
  default:
    platforms: []
    packages:
      all: [foo]
"""
        }, check)


def test_lock_file_has_wrong_hash():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert [] == project.problems
        assert [
            "anaconda-project-lock.yml: Env spec 'default' has changed since the lock "
            'file was last updated (env spec hash has changed from wrong to '
            'a30f02c961ef4f3fe07ceb09e0906394c3885a79)'
        ] == project.suggestions

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_LOCK_FILENAME:
        """
env_specs:
  default:
    env_spec_hash: wrong
    platforms: [linux-64,osx-64,win-64]
    packages:
      all: [foo]
"""
    }, check)


def test_lock_file_has_empty_and_wrong_package_lists():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert [] == project.problems
        # yapf: disable
        assert [
            'anaconda-project-lock.yml: Lock file lists no packages for env spec '
            "'default' on platform linux-64",
            'anaconda-project-lock.yml: Lock file is missing 1 packages for env spec '
            'default on osx-64 (hello)',
            'anaconda-project-lock.yml: Lock file lists no packages for env spec '
            "'default' on platform win-64"
        ] == project.suggestions
        # yapf: enable

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
env_specs:
   default:
      packages:
         - hello
""",
        DEFAULT_PROJECT_LOCK_FILENAME:
        """
env_specs:
  default:
    platforms: [linux-64,osx-64,win-64]
    packages:
      linux-64: []
      win-64: []
      win-32: [foo]
      osx-64: [bar]
      linux-32: [baz]
"""
    }, check)


def test_lock_file_has_orphan_env_spec():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert [] == project.problems
        assert ["anaconda-project-lock.yml: Lock file lists env spec 'orphan' which is not in anaconda-project.yml"
                ] == project.suggestions

    with_directory_contents_completing_project_file({
        DEFAULT_PROJECT_FILENAME:
        """
env_specs:
   default:
      packages:
         - hello
""",
        DEFAULT_PROJECT_LOCK_FILENAME:
        """
env_specs:
  default:
    platforms: [linux-64,osx-64,win-64]
    packages:
      all: [hello]
  orphan:
    platforms: [linux-64,osx-64,win-64]
    packages: {}
"""
    }, check)


def test_fix_project_file_with_no_platforms():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert ["anaconda-project.yml: The 'platforms:' field should list platforms the project supports."
                ] == project.problems

        assert () == project.env_specs['default'].platforms

        project.fix_problems_and_suggestions()

        assert [] == project.problems

        assert ('linux-64', 'osx-64', 'win-64') == project.env_specs['default'].platforms

    with_directory_contents(
        {
            DEFAULT_PROJECT_FILENAME: """
name: foo
env_specs:
   default:
      packages:
         - hello
""",
            DEFAULT_PROJECT_LOCK_FILENAME: """
locking_enabled: true
"""
        }, check)


def test_fix_env_spec_with_no_platforms():
    def check(dirname):
        project = project_no_dedicated_env(dirname)

        assert [
            "anaconda-project.yml: Env spec bar does not have anything in its 'platforms:' field.",
            "anaconda-project.yml: Env spec foo does not have anything in its 'platforms:' field."
        ] == project.problems

        assert ('linux-64', ) == project.env_specs['default'].platforms
        assert () == project.env_specs['foo'].platforms
        assert () == project.env_specs['bar'].platforms

        project.fix_problems_and_suggestions()

        assert [] == project.problems

        assert ('linux-64', ) == project.env_specs['default'].platforms
        assert ('linux-64', 'osx-64', 'win-64') == project.env_specs['foo'].platforms
        assert ('linux-64', 'osx-64', 'win-64') == project.env_specs['bar'].platforms

    with_directory_contents({
        DEFAULT_PROJECT_FILENAME:
        """
name: foo
env_specs:
   default:
      platforms: ['linux-64']
      packages:
         - hello
   foo:
      platforms: []
      packages:
         - apackage
   bar:
      platforms: []
      packages:
         - package2
""",
        DEFAULT_PROJECT_LOCK_FILENAME:
        """
locking_enabled: true
"""
    }, check)
